diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index af75ace8bd4..31e249ec806 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -29,7 +29,6 @@ from .const import ( POWERWALL_API_CHANGED, POWERWALL_COORDINATOR, POWERWALL_HTTP_SESSION, - POWERWALL_LOGIN_FAILED_COUNT, UPDATE_INTERVAL, ) from .models import PowerwallBaseInfo, PowerwallData, PowerwallRuntimeData @@ -40,8 +39,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -MAX_LOGIN_FAILURES = 5 - API_CHANGED_ERROR_BODY = ( "It seems like your powerwall uses an unsupported version. " "Please update the software of your powerwall or if it is " @@ -68,29 +65,15 @@ class PowerwallDataManager: self.runtime_data = runtime_data self.power_wall = power_wall - @property - def login_failed_count(self) -> int: - """Return the current number of failed logins.""" - return self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] - @property def api_changed(self) -> int: """Return true if the api has changed out from under us.""" return self.runtime_data[POWERWALL_API_CHANGED] - def _increment_failed_logins(self) -> None: - self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] += 1 - - def _clear_failed_logins(self) -> None: - self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] = 0 - def _recreate_powerwall_login(self) -> None: """Recreate the login on auth failure.""" - http_session = self.runtime_data[POWERWALL_HTTP_SESSION] - http_session.close() - http_session = requests.Session() - self.runtime_data[POWERWALL_HTTP_SESSION] = http_session - self.power_wall = Powerwall(self.ip_address, http_session=http_session) + if self.power_wall.is_authenticated(): + self.power_wall.logout() self.power_wall.login(self.password or "") async def async_update_data(self) -> PowerwallData: @@ -121,17 +104,15 @@ class PowerwallDataManager: raise UpdateFailed("The powerwall api has changed") from err except AccessDeniedError as err: if attempt == 1: - self._increment_failed_logins() + # failed to authenticate => the credentials must be wrong raise ConfigEntryAuthFailed from err if self.password is None: raise ConfigEntryAuthFailed from err - raise UpdateFailed( - f"Login attempt {self.login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry: {err}" - ) from err + _LOGGER.debug("Access denied, trying to reauthenticate") + # there is still an attempt left to authenticate, so we continue in the loop except APIError as err: raise UpdateFailed(f"Updated failed due to {err}, will retry") from err else: - self._clear_failed_logins() return data raise RuntimeError("unreachable") @@ -174,7 +155,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_changed=False, base_info=base_info, http_session=http_session, - login_failed_count=0, coordinator=None, ) diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index b1791c4300e..9df710e2df4 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -7,7 +7,6 @@ POWERWALL_BASE_INFO: Final = "base_info" POWERWALL_COORDINATOR: Final = "coordinator" POWERWALL_API_CHANGED: Final = "api_changed" POWERWALL_HTTP_SESSION: Final = "http_session" -POWERWALL_LOGIN_FAILED_COUNT: Final = "login_failed_count" UPDATE_INTERVAL = 30 diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py index 6f8ccb98459..522dc2806bf 100644 --- a/homeassistant/components/powerwall/models.py +++ b/homeassistant/components/powerwall/models.py @@ -45,7 +45,6 @@ class PowerwallRuntimeData(TypedDict): """Run time data for the powerwall.""" coordinator: DataUpdateCoordinator | None - login_failed_count: int base_info: PowerwallBaseInfo api_changed: bool http_session: Session diff --git a/tests/components/powerwall/test_init.py b/tests/components/powerwall/test_init.py new file mode 100644 index 00000000000..d5fa92fc23f --- /dev/null +++ b/tests/components/powerwall/test_init.py @@ -0,0 +1,66 @@ +"""Tests for the PowerwallDataManager.""" + +import datetime +from unittest.mock import MagicMock, patch + +from tesla_powerwall import AccessDeniedError, LoginResponse + +from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .mocks import _mock_powerwall_with_fixtures + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_update_data_reauthenticate_on_access_denied(hass: HomeAssistant): + """Test if _update_data of PowerwallDataManager reauthenticates on AccessDeniedError.""" + + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + # login responses for the different tests: + # 1. login success on entry setup + # 2. login success after reauthentication + # 3. login failure after reauthentication + mock_powerwall.login = MagicMock(name="login", return_value=LoginResponse({})) + mock_powerwall.get_charge = MagicMock(name="get_charge", return_value=90.0) + mock_powerwall.is_authenticated = MagicMock( + name="is_authenticated", return_value=True + ) + mock_powerwall.logout = MagicMock(name="logout") + + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "password"} + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + mock_powerwall.login.reset_mock(return_value=True) + mock_powerwall.get_charge.side_effect = [AccessDeniedError("test"), 90.0] + + async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=1)) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress(DOMAIN) + assert len(flows) == 0 + + mock_powerwall.login.reset_mock() + mock_powerwall.login.side_effect = AccessDeniedError("test") + mock_powerwall.get_charge.side_effect = [AccessDeniedError("test"), 90.0] + + async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=1)) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress(DOMAIN) + assert len(flows) == 1 + reauth_flow = flows[0] + assert reauth_flow["context"]["source"] == "reauth"