From f6bc5c98b3b13bc7aa9eccc3408e9f1f067a252f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Jan 2024 13:30:21 -1000 Subject: [PATCH] Handle tplink credential change at run time (#108692) --- .../components/tplink/coordinator.py | 5 ++- tests/components/tplink/test_init.py | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index 582c49638e7..798580ef3c2 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -4,9 +4,10 @@ from __future__ import annotations from datetime import timedelta import logging -from kasa import SmartDevice, SmartDeviceException +from kasa import AuthenticationException, SmartDevice, SmartDeviceException from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -42,5 +43,7 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): """Fetch all device and sensor data from api.""" try: await self.device.update(update_children=False) + except AuthenticationException as ex: + raise ConfigEntryAuthFailed from ex except SmartDeviceException as ex: raise UpdateFailed(ex) from ex diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index e6297cf6553..7bee7823013 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -5,6 +5,7 @@ import copy from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch +from kasa.exceptions import AuthenticationException import pytest from homeassistant import setup @@ -17,6 +18,8 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STARTED, + STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry @@ -29,6 +32,7 @@ from . import ( IP_ADDRESS, MAC_ADDRESS, _mocked_dimmer, + _mocked_plug, _patch_connect, _patch_discovery, _patch_single_discovery, @@ -256,3 +260,32 @@ async def test_config_entry_errors( any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) == reauth_flows ) + + +async def test_plug_auth_fails(hass: HomeAssistant) -> None: + """Test a smart plug auth failure.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry.add_to_hass(hass) + plug = _mocked_plug() + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.my_plug" + state = hass.states.get(entity_id) + assert state.state == STATE_ON + plug.update = AsyncMock(side_effect=AuthenticationException) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + assert ( + len( + hass.config_entries.flow.async_progress_by_handler( + DOMAIN, match_context={"source": SOURCE_REAUTH} + ) + ) + == 1 + )