diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 1e750131cc2..f53dfeafe48 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -6,15 +6,23 @@ from typing import Any import async_timeout from devolo_plc_api import Device -from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo -from devolo_plc_api.exceptions.device import DeviceNotFound, DeviceUnavailable +from devolo_plc_api.device_api import ( + ConnectedStationInfo, + NeighborAPInfo, + WifiGuestAccessGet, +) +from devolo_plc_api.exceptions.device import ( + DeviceNotFound, + DevicePasswordProtected, + DeviceUnavailable, +) from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -26,6 +34,8 @@ from .const import ( NEIGHBORING_WIFI_NETWORKS, PLATFORMS, SHORT_UPDATE_INTERVAL, + SWITCH_GUEST_WIFI, + SWITCH_LEDS, ) _LOGGER = logging.getLogger(__name__) @@ -59,6 +69,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except DeviceUnavailable as err: raise UpdateFailed(err) from err + async def async_update_guest_wifi_status() -> WifiGuestAccessGet: + """Fetch data from API endpoint.""" + assert device.device + try: + async with async_timeout.timeout(10): + return await device.device.async_get_wifi_guest_access() + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + except DevicePasswordProtected as err: + raise ConfigEntryAuthFailed(err) from err + + async def async_update_led_status() -> bool: + """Fetch data from API endpoint.""" + assert device.device + try: + async with async_timeout.timeout(10): + return await device.device.async_get_led_setting() + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + except DevicePasswordProtected as err: + raise ConfigEntryAuthFailed(err) from err + async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]: """Fetch data from API endpoint.""" assert device.device @@ -90,6 +122,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_connected_plc_devices, update_interval=LONG_UPDATE_INTERVAL, ) + if device.device and "led" in device.device.features: + coordinators[SWITCH_LEDS] = DataUpdateCoordinator( + hass, + _LOGGER, + name=SWITCH_LEDS, + update_method=async_update_led_status, + update_interval=SHORT_UPDATE_INTERVAL, + ) if device.device and "wifi1" in device.device.features: coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator( hass, @@ -105,6 +145,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_wifi_neighbor_access_points, update_interval=LONG_UPDATE_INTERVAL, ) + coordinators[SWITCH_GUEST_WIFI] = DataUpdateCoordinator( + hass, + _LOGGER, + name=SWITCH_GUEST_WIFI, + update_method=async_update_guest_wifi_status, + update_interval=SHORT_UPDATE_INTERVAL, + ) hass.data[DOMAIN][entry.entry_id] = {"device": device, "coordinators": coordinators} diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 94794e0403d..ba174d30abd 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -72,10 +72,10 @@ async def async_setup_entry( if device.plcnet: entities.append( DevoloBinarySensorEntity( + entry, coordinators[CONNECTED_PLC_DEVICES], SENSOR_TYPES[CONNECTED_TO_ROUTER], device, - entry.title, ) ) async_add_entities(entities) @@ -86,14 +86,14 @@ class DevoloBinarySensorEntity(DevoloEntity[LogicalNetwork], BinarySensorEntity) def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator[LogicalNetwork], description: DevoloBinarySensorEntityDescription, device: Device, - device_name: str, ) -> None: """Initialize entity.""" self.entity_description: DevoloBinarySensorEntityDescription = description - super().__init__(coordinator, device, device_name) + super().__init__(entry, coordinator, device) @property def is_on(self) -> bool: diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index c591dfb086c..fffe9b5d482 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -12,7 +12,12 @@ from devolo_plc_api.device_api import ( from homeassistant.const import Platform DOMAIN = "devolo_home_network" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SWITCH, +] PRODUCT = "product" SERIAL_NUMBER = "serial_number" @@ -25,6 +30,8 @@ CONNECTED_PLC_DEVICES = "connected_plc_devices" CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" +SWITCH_GUEST_WIFI = "switch_guest_wifi" +SWITCH_LEDS = "switch_leds" WIFI_APTYPE = { WIFI_VAP_MAIN_AP: "Main", diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index a7ded44884d..b5a10a108b2 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -4,9 +4,14 @@ from __future__ import annotations from typing import TypeVar, Union from devolo_plc_api.device import Device -from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo +from devolo_plc_api.device_api import ( + ConnectedStationInfo, + NeighborAPInfo, + WifiGuestAccessGet, +) from devolo_plc_api.plcnet_api import LogicalNetwork +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -21,6 +26,8 @@ _DataT = TypeVar( LogicalNetwork, list[ConnectedStationInfo], list[NeighborAPInfo], + WifiGuestAccessGet, + bool, ], ) @@ -32,21 +39,22 @@ class DevoloEntity(CoordinatorEntity[DataUpdateCoordinator[_DataT]]): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator[_DataT], device: Device, - device_name: str, ) -> None: """Initialize a devolo home network device.""" super().__init__(coordinator) self.device = device + self.entry = entry self._attr_device_info = DeviceInfo( configuration_url=f"http://{device.ip}", identifiers={(DOMAIN, str(device.serial_number))}, manufacturer="devolo", model=device.product, - name=device_name, + name=entry.title, sw_version=device.firmware_version, ) self._attr_unique_id = f"{device.serial_number}_{self.entity_description.key}" diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index b2382874aa0..e59f856e8da 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -65,7 +65,6 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any]] = { ), CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription[list[ConnectedStationInfo]]( key=CONNECTED_WIFI_CLIENTS, - entity_registry_enabled_default=True, icon="mdi:wifi", name="Connected Wifi clients", state_class=SensorStateClass.MEASUREMENT, @@ -95,27 +94,27 @@ async def async_setup_entry( if device.plcnet: entities.append( DevoloSensorEntity( + entry, coordinators[CONNECTED_PLC_DEVICES], SENSOR_TYPES[CONNECTED_PLC_DEVICES], device, - entry.title, ) ) if device.device and "wifi1" in device.device.features: entities.append( DevoloSensorEntity( + entry, coordinators[CONNECTED_WIFI_CLIENTS], SENSOR_TYPES[CONNECTED_WIFI_CLIENTS], device, - entry.title, ) ) entities.append( DevoloSensorEntity( + entry, coordinators[NEIGHBORING_WIFI_NETWORKS], SENSOR_TYPES[NEIGHBORING_WIFI_NETWORKS], device, - entry.title, ) ) async_add_entities(entities) @@ -128,14 +127,14 @@ class DevoloSensorEntity(DevoloEntity[_DataT], SensorEntity): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator[_DataT], description: DevoloSensorEntityDescription[_DataT], device: Device, - device_name: str, ) -> None: """Initialize entity.""" self.entity_description = description - super().__init__(coordinator, device, device_name) + super().__init__(entry, coordinator, device) @property def native_value(self) -> int: diff --git a/homeassistant/components/devolo_home_network/switch.py b/homeassistant/components/devolo_home_network/switch.py new file mode 100644 index 00000000000..8b018f01948 --- /dev/null +++ b/homeassistant/components/devolo_home_network/switch.py @@ -0,0 +1,138 @@ +"""Platform for switch integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, Generic, TypeVar, Union + +from devolo_plc_api.device import Device +from devolo_plc_api.device_api import WifiGuestAccessGet +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS +from .entity import DevoloEntity + +_DataT = TypeVar( + "_DataT", + bound=Union[ + WifiGuestAccessGet, + bool, + ], +) + + +@dataclass +class DevoloSwitchRequiredKeysMixin(Generic[_DataT]): + """Mixin for required keys.""" + + is_on_func: Callable[[_DataT], bool] + turn_on_func: Callable[[Device], Awaitable[bool]] + turn_off_func: Callable[[Device], Awaitable[bool]] + + +@dataclass +class DevoloSwitchEntityDescription( + SwitchEntityDescription, DevoloSwitchRequiredKeysMixin[_DataT] +): + """Describes devolo switch entity.""" + + +SWITCH_TYPES: dict[str, DevoloSwitchEntityDescription[Any]] = { + SWITCH_GUEST_WIFI: DevoloSwitchEntityDescription[WifiGuestAccessGet]( + key=SWITCH_GUEST_WIFI, + icon="mdi:wifi", + name="Enable guest Wifi", + is_on_func=lambda data: data.enabled is True, + turn_on_func=lambda device: device.device.async_set_wifi_guest_access(True), # type: ignore[union-attr] + turn_off_func=lambda device: device.device.async_set_wifi_guest_access(False), # type: ignore[union-attr] + ), + SWITCH_LEDS: DevoloSwitchEntityDescription[bool]( + key=SWITCH_LEDS, + entity_category=EntityCategory.CONFIG, + icon="mdi:led-off", + name="Enable LEDs", + is_on_func=bool, + turn_on_func=lambda device: device.device.async_set_led_setting(True), # type: ignore[union-attr] + turn_off_func=lambda device: device.device.async_set_led_setting(False), # type: ignore[union-attr] + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] + + entities: list[DevoloSwitchEntity[Any]] = [] + if device.device and "led" in device.device.features: + entities.append( + DevoloSwitchEntity( + entry, + coordinators[SWITCH_LEDS], + SWITCH_TYPES[SWITCH_LEDS], + device, + ) + ) + if device.device and "wifi1" in device.device.features: + entities.append( + DevoloSwitchEntity( + entry, + coordinators[SWITCH_GUEST_WIFI], + SWITCH_TYPES[SWITCH_GUEST_WIFI], + device, + ) + ) + async_add_entities(entities) + + +class DevoloSwitchEntity(DevoloEntity[_DataT], SwitchEntity): + """Representation of a devolo switch.""" + + entity_description: DevoloSwitchEntityDescription[_DataT] + + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[_DataT], + description: DevoloSwitchEntityDescription[_DataT], + device: Device, + ) -> None: + """Initialize entity.""" + self.entity_description = description + super().__init__(entry, coordinator, device) + + @property + def is_on(self) -> bool: + """State of the switch.""" + return self.entity_description.is_on_func(self.coordinator.data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + try: + await self.entity_description.turn_on_func(self.device) + except DevicePasswordProtected: + self.entry.async_start_reauth(self.hass) + except DeviceUnavailable: + pass # The coordinator will handle this + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + try: + await self.entity_description.turn_off_func(self.device) + except DevicePasswordProtected: + self.entry.async_start_reauth(self.hass) + except DeviceUnavailable: + pass # The coordinator will handle this + await self.coordinator.async_request_refresh() diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index c9f9270f0b5..1672c701e66 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -6,6 +6,7 @@ from devolo_plc_api.device_api import ( WIFI_VAP_MAIN_AP, ConnectedStationInfo, NeighborAPInfo, + WifiGuestAccessGet, ) from devolo_plc_api.plcnet_api import LogicalNetwork @@ -56,6 +57,13 @@ DISCOVERY_INFO_WRONG_DEVICE = ZeroconfServiceInfo( type="mock_type", ) +GUEST_WIFI = WifiGuestAccessGet( + ssid="devolo-guest-930", + key="HMANPGBA", + enabled=False, + remaining_duration=0, +) + NEIGHBOR_ACCESS_POINTS = [ NeighborAPInfo( mac_address="AA:BB:CC:DD:EE:FF", @@ -67,7 +75,6 @@ NEIGHBOR_ACCESS_POINTS = [ ) ] - PLCNET = LogicalNetwork( devices=[ { @@ -85,7 +92,6 @@ PLCNET = LogicalNetwork( ], ) - PLCNET_ATTACHED = LogicalNetwork( devices=[ { diff --git a/tests/components/devolo_home_network/mock.py b/tests/components/devolo_home_network/mock.py index 0a0a6c2dd4e..8dcb785aaea 100644 --- a/tests/components/devolo_home_network/mock.py +++ b/tests/components/devolo_home_network/mock.py @@ -13,6 +13,7 @@ from zeroconf.asyncio import AsyncZeroconf from .const import ( CONNECTED_STATIONS, DISCOVERY_INFO, + GUEST_WIFI, IP, NEIGHBOR_ACCESS_POINTS, PLCNET, @@ -43,9 +44,11 @@ class MockDevice(Device): """Reset mock to starting point.""" self.async_disconnect = AsyncMock() self.device = DeviceApi(IP, None, DISCOVERY_INFO) + self.device.async_get_led_setting = AsyncMock(return_value=False) self.device.async_get_wifi_connected_station = AsyncMock( return_value=CONNECTED_STATIONS ) + self.device.async_get_wifi_guest_access = AsyncMock(return_value=GUEST_WIFI) self.device.async_get_wifi_neighbor_access_points = AsyncMock( return_value=NEIGHBOR_ACCESS_POINTS ) diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py new file mode 100644 index 00000000000..a7853ad4b55 --- /dev/null +++ b/tests/components/devolo_home_network/test_switch.py @@ -0,0 +1,351 @@ +"""Tests for the devolo Home Network switch.""" +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from devolo_plc_api.device_api import WifiGuestAccessGet +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable +import pytest + +from homeassistant.components.devolo_home_network.const import ( + DOMAIN, + SHORT_UPDATE_INTERVAL, +) +from homeassistant.components.switch import DOMAIN as PLATFORM +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.update_coordinator import REQUEST_REFRESH_DEFAULT_COOLDOWN +from homeassistant.util import dt + +from . import configure_integration +from .mock import MockDevice + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("mock_device") +async def test_switch_setup(hass: HomeAssistant): + """Test default setup of the switch component.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{PLATFORM}.{device_name}_enable_guest_wifi") is not None + assert hass.states.get(f"{PLATFORM}.{device_name}_enable_leds") is not None + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_guest_wifi_status_auth_failed( + hass: HomeAssistant, mock_device: MockDevice +): + """Test getting the wifi_status with wrong password triggers the reauth flow.""" + entry = configure_integration(hass) + mock_device.device.async_get_wifi_guest_access.side_effect = DevicePasswordProtected + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_led_status_auth_failed( + hass: HomeAssistant, mock_device: MockDevice +): + """Test getting the led status with wrong password triggers the reauth flow.""" + entry = configure_integration(hass) + mock_device.device.async_get_led_setting.side_effect = DevicePasswordProtected + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_enable_guest_wifi(hass: HomeAssistant, mock_device: MockDevice): + """Test state change of a enable_guest_wifi switch device.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_enable_guest_wifi" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + # Emulate state change + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=True + ) + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + # Switch off + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=False + ) + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + new=AsyncMock(), + ) as turn_off: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + turn_off.assert_called_once_with(False) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Switch on + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=True + ) + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + new=AsyncMock(), + ) as turn_on: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + turn_on.assert_called_once_with(True) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Device unavailable + mock_device.device.async_get_wifi_guest_access.side_effect = DeviceUnavailable() + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + side_effect=DeviceUnavailable, + ): + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_enable_leds(hass: HomeAssistant, mock_device: MockDevice): + """Test state change of a enable_leds switch device.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_enable_leds" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + er = entity_registry.async_get(hass) + assert er.async_get(state_key).entity_category == EntityCategory.CONFIG + + # Emulate state change + mock_device.device.async_get_led_setting.return_value = True + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + # Switch off + mock_device.device.async_get_led_setting.return_value = False + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + new=AsyncMock(), + ) as turn_off: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + turn_off.assert_called_once_with(False) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Switch on + mock_device.device.async_get_led_setting.return_value = True + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + new=AsyncMock(), + ) as turn_on: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + turn_on.assert_called_once_with(True) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Device unavailable + mock_device.device.async_get_led_setting.side_effect = DeviceUnavailable() + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + side_effect=DeviceUnavailable, + ): + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.parametrize( + "name, get_method, update_interval", + [ + ["enable_guest_wifi", "async_get_wifi_guest_access", SHORT_UPDATE_INTERVAL], + ["enable_leds", "async_get_led_setting", SHORT_UPDATE_INTERVAL], + ], +) +async def test_device_failure( + hass: HomeAssistant, + mock_device: MockDevice, + name: str, + get_method: str, + update_interval: timedelta, +): + """Test device failure.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_{name}" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + + api = getattr(mock_device.device, get_method) + api.side_effect = DeviceUnavailable + async_fire_time_changed(hass, dt.utcnow() + update_interval) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + "name, set_method", + [ + ["enable_guest_wifi", "async_set_wifi_guest_access"], + ["enable_leds", "async_set_led_setting"], + ], +) +async def test_auth_failed( + hass: HomeAssistant, mock_device: MockDevice, name: str, set_method: str +): + """Test setting unautherized triggers the reauth flow.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_{name}" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + + setattr(mock_device.device, set_method, AsyncMock()) + api = getattr(mock_device.device, set_method) + api.side_effect = DevicePasswordProtected + + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id)