mirror of
https://github.com/home-assistant/core.git
synced 2025-07-12 15:57:06 +00:00
Add switch platform to devolo_home_network (#72494)
This commit is contained in:
parent
05c32c51fd
commit
d40a4aa970
@ -6,15 +6,23 @@ from typing import Any
|
|||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
from devolo_plc_api import Device
|
from devolo_plc_api import Device
|
||||||
from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo
|
from devolo_plc_api.device_api import (
|
||||||
from devolo_plc_api.exceptions.device import DeviceNotFound, DeviceUnavailable
|
ConnectedStationInfo,
|
||||||
|
NeighborAPInfo,
|
||||||
|
WifiGuestAccessGet,
|
||||||
|
)
|
||||||
|
from devolo_plc_api.exceptions.device import (
|
||||||
|
DeviceNotFound,
|
||||||
|
DevicePasswordProtected,
|
||||||
|
DeviceUnavailable,
|
||||||
|
)
|
||||||
from devolo_plc_api.plcnet_api import LogicalNetwork
|
from devolo_plc_api.plcnet_api import LogicalNetwork
|
||||||
|
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant
|
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.httpx_client import get_async_client
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@ -26,6 +34,8 @@ from .const import (
|
|||||||
NEIGHBORING_WIFI_NETWORKS,
|
NEIGHBORING_WIFI_NETWORKS,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SHORT_UPDATE_INTERVAL,
|
SHORT_UPDATE_INTERVAL,
|
||||||
|
SWITCH_GUEST_WIFI,
|
||||||
|
SWITCH_LEDS,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -59,6 +69,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
except DeviceUnavailable as err:
|
except DeviceUnavailable as err:
|
||||||
raise UpdateFailed(err) from 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]:
|
async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]:
|
||||||
"""Fetch data from API endpoint."""
|
"""Fetch data from API endpoint."""
|
||||||
assert device.device
|
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_method=async_update_connected_plc_devices,
|
||||||
update_interval=LONG_UPDATE_INTERVAL,
|
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:
|
if device.device and "wifi1" in device.device.features:
|
||||||
coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator(
|
coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
@ -105,6 +145,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
update_method=async_update_wifi_neighbor_access_points,
|
update_method=async_update_wifi_neighbor_access_points,
|
||||||
update_interval=LONG_UPDATE_INTERVAL,
|
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}
|
hass.data[DOMAIN][entry.entry_id] = {"device": device, "coordinators": coordinators}
|
||||||
|
|
||||||
|
@ -72,10 +72,10 @@ async def async_setup_entry(
|
|||||||
if device.plcnet:
|
if device.plcnet:
|
||||||
entities.append(
|
entities.append(
|
||||||
DevoloBinarySensorEntity(
|
DevoloBinarySensorEntity(
|
||||||
|
entry,
|
||||||
coordinators[CONNECTED_PLC_DEVICES],
|
coordinators[CONNECTED_PLC_DEVICES],
|
||||||
SENSOR_TYPES[CONNECTED_TO_ROUTER],
|
SENSOR_TYPES[CONNECTED_TO_ROUTER],
|
||||||
device,
|
device,
|
||||||
entry.title,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
@ -86,14 +86,14 @@ class DevoloBinarySensorEntity(DevoloEntity[LogicalNetwork], BinarySensorEntity)
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
entry: ConfigEntry,
|
||||||
coordinator: DataUpdateCoordinator[LogicalNetwork],
|
coordinator: DataUpdateCoordinator[LogicalNetwork],
|
||||||
description: DevoloBinarySensorEntityDescription,
|
description: DevoloBinarySensorEntityDescription,
|
||||||
device: Device,
|
device: Device,
|
||||||
device_name: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize entity."""
|
"""Initialize entity."""
|
||||||
self.entity_description: DevoloBinarySensorEntityDescription = description
|
self.entity_description: DevoloBinarySensorEntityDescription = description
|
||||||
super().__init__(coordinator, device, device_name)
|
super().__init__(entry, coordinator, device)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
|
@ -12,7 +12,12 @@ from devolo_plc_api.device_api import (
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
DOMAIN = "devolo_home_network"
|
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"
|
PRODUCT = "product"
|
||||||
SERIAL_NUMBER = "serial_number"
|
SERIAL_NUMBER = "serial_number"
|
||||||
@ -25,6 +30,8 @@ CONNECTED_PLC_DEVICES = "connected_plc_devices"
|
|||||||
CONNECTED_TO_ROUTER = "connected_to_router"
|
CONNECTED_TO_ROUTER = "connected_to_router"
|
||||||
CONNECTED_WIFI_CLIENTS = "connected_wifi_clients"
|
CONNECTED_WIFI_CLIENTS = "connected_wifi_clients"
|
||||||
NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks"
|
NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks"
|
||||||
|
SWITCH_GUEST_WIFI = "switch_guest_wifi"
|
||||||
|
SWITCH_LEDS = "switch_leds"
|
||||||
|
|
||||||
WIFI_APTYPE = {
|
WIFI_APTYPE = {
|
||||||
WIFI_VAP_MAIN_AP: "Main",
|
WIFI_VAP_MAIN_AP: "Main",
|
||||||
|
@ -4,9 +4,14 @@ from __future__ import annotations
|
|||||||
from typing import TypeVar, Union
|
from typing import TypeVar, Union
|
||||||
|
|
||||||
from devolo_plc_api.device import Device
|
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 devolo_plc_api.plcnet_api import LogicalNetwork
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
CoordinatorEntity,
|
CoordinatorEntity,
|
||||||
@ -21,6 +26,8 @@ _DataT = TypeVar(
|
|||||||
LogicalNetwork,
|
LogicalNetwork,
|
||||||
list[ConnectedStationInfo],
|
list[ConnectedStationInfo],
|
||||||
list[NeighborAPInfo],
|
list[NeighborAPInfo],
|
||||||
|
WifiGuestAccessGet,
|
||||||
|
bool,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,21 +39,22 @@ class DevoloEntity(CoordinatorEntity[DataUpdateCoordinator[_DataT]]):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
entry: ConfigEntry,
|
||||||
coordinator: DataUpdateCoordinator[_DataT],
|
coordinator: DataUpdateCoordinator[_DataT],
|
||||||
device: Device,
|
device: Device,
|
||||||
device_name: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a devolo home network device."""
|
"""Initialize a devolo home network device."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self.device = device
|
self.device = device
|
||||||
|
self.entry = entry
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
configuration_url=f"http://{device.ip}",
|
configuration_url=f"http://{device.ip}",
|
||||||
identifiers={(DOMAIN, str(device.serial_number))},
|
identifiers={(DOMAIN, str(device.serial_number))},
|
||||||
manufacturer="devolo",
|
manufacturer="devolo",
|
||||||
model=device.product,
|
model=device.product,
|
||||||
name=device_name,
|
name=entry.title,
|
||||||
sw_version=device.firmware_version,
|
sw_version=device.firmware_version,
|
||||||
)
|
)
|
||||||
self._attr_unique_id = f"{device.serial_number}_{self.entity_description.key}"
|
self._attr_unique_id = f"{device.serial_number}_{self.entity_description.key}"
|
||||||
|
@ -65,7 +65,6 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any]] = {
|
|||||||
),
|
),
|
||||||
CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription[list[ConnectedStationInfo]](
|
CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription[list[ConnectedStationInfo]](
|
||||||
key=CONNECTED_WIFI_CLIENTS,
|
key=CONNECTED_WIFI_CLIENTS,
|
||||||
entity_registry_enabled_default=True,
|
|
||||||
icon="mdi:wifi",
|
icon="mdi:wifi",
|
||||||
name="Connected Wifi clients",
|
name="Connected Wifi clients",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
@ -95,27 +94,27 @@ async def async_setup_entry(
|
|||||||
if device.plcnet:
|
if device.plcnet:
|
||||||
entities.append(
|
entities.append(
|
||||||
DevoloSensorEntity(
|
DevoloSensorEntity(
|
||||||
|
entry,
|
||||||
coordinators[CONNECTED_PLC_DEVICES],
|
coordinators[CONNECTED_PLC_DEVICES],
|
||||||
SENSOR_TYPES[CONNECTED_PLC_DEVICES],
|
SENSOR_TYPES[CONNECTED_PLC_DEVICES],
|
||||||
device,
|
device,
|
||||||
entry.title,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if device.device and "wifi1" in device.device.features:
|
if device.device and "wifi1" in device.device.features:
|
||||||
entities.append(
|
entities.append(
|
||||||
DevoloSensorEntity(
|
DevoloSensorEntity(
|
||||||
|
entry,
|
||||||
coordinators[CONNECTED_WIFI_CLIENTS],
|
coordinators[CONNECTED_WIFI_CLIENTS],
|
||||||
SENSOR_TYPES[CONNECTED_WIFI_CLIENTS],
|
SENSOR_TYPES[CONNECTED_WIFI_CLIENTS],
|
||||||
device,
|
device,
|
||||||
entry.title,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
entities.append(
|
entities.append(
|
||||||
DevoloSensorEntity(
|
DevoloSensorEntity(
|
||||||
|
entry,
|
||||||
coordinators[NEIGHBORING_WIFI_NETWORKS],
|
coordinators[NEIGHBORING_WIFI_NETWORKS],
|
||||||
SENSOR_TYPES[NEIGHBORING_WIFI_NETWORKS],
|
SENSOR_TYPES[NEIGHBORING_WIFI_NETWORKS],
|
||||||
device,
|
device,
|
||||||
entry.title,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
@ -128,14 +127,14 @@ class DevoloSensorEntity(DevoloEntity[_DataT], SensorEntity):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
entry: ConfigEntry,
|
||||||
coordinator: DataUpdateCoordinator[_DataT],
|
coordinator: DataUpdateCoordinator[_DataT],
|
||||||
description: DevoloSensorEntityDescription[_DataT],
|
description: DevoloSensorEntityDescription[_DataT],
|
||||||
device: Device,
|
device: Device,
|
||||||
device_name: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize entity."""
|
"""Initialize entity."""
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
super().__init__(coordinator, device, device_name)
|
super().__init__(entry, coordinator, device)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int:
|
def native_value(self) -> int:
|
||||||
|
138
homeassistant/components/devolo_home_network/switch.py
Normal file
138
homeassistant/components/devolo_home_network/switch.py
Normal file
@ -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()
|
@ -6,6 +6,7 @@ from devolo_plc_api.device_api import (
|
|||||||
WIFI_VAP_MAIN_AP,
|
WIFI_VAP_MAIN_AP,
|
||||||
ConnectedStationInfo,
|
ConnectedStationInfo,
|
||||||
NeighborAPInfo,
|
NeighborAPInfo,
|
||||||
|
WifiGuestAccessGet,
|
||||||
)
|
)
|
||||||
from devolo_plc_api.plcnet_api import LogicalNetwork
|
from devolo_plc_api.plcnet_api import LogicalNetwork
|
||||||
|
|
||||||
@ -56,6 +57,13 @@ DISCOVERY_INFO_WRONG_DEVICE = ZeroconfServiceInfo(
|
|||||||
type="mock_type",
|
type="mock_type",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GUEST_WIFI = WifiGuestAccessGet(
|
||||||
|
ssid="devolo-guest-930",
|
||||||
|
key="HMANPGBA",
|
||||||
|
enabled=False,
|
||||||
|
remaining_duration=0,
|
||||||
|
)
|
||||||
|
|
||||||
NEIGHBOR_ACCESS_POINTS = [
|
NEIGHBOR_ACCESS_POINTS = [
|
||||||
NeighborAPInfo(
|
NeighborAPInfo(
|
||||||
mac_address="AA:BB:CC:DD:EE:FF",
|
mac_address="AA:BB:CC:DD:EE:FF",
|
||||||
@ -67,7 +75,6 @@ NEIGHBOR_ACCESS_POINTS = [
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
PLCNET = LogicalNetwork(
|
PLCNET = LogicalNetwork(
|
||||||
devices=[
|
devices=[
|
||||||
{
|
{
|
||||||
@ -85,7 +92,6 @@ PLCNET = LogicalNetwork(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
PLCNET_ATTACHED = LogicalNetwork(
|
PLCNET_ATTACHED = LogicalNetwork(
|
||||||
devices=[
|
devices=[
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ from zeroconf.asyncio import AsyncZeroconf
|
|||||||
from .const import (
|
from .const import (
|
||||||
CONNECTED_STATIONS,
|
CONNECTED_STATIONS,
|
||||||
DISCOVERY_INFO,
|
DISCOVERY_INFO,
|
||||||
|
GUEST_WIFI,
|
||||||
IP,
|
IP,
|
||||||
NEIGHBOR_ACCESS_POINTS,
|
NEIGHBOR_ACCESS_POINTS,
|
||||||
PLCNET,
|
PLCNET,
|
||||||
@ -43,9 +44,11 @@ class MockDevice(Device):
|
|||||||
"""Reset mock to starting point."""
|
"""Reset mock to starting point."""
|
||||||
self.async_disconnect = AsyncMock()
|
self.async_disconnect = AsyncMock()
|
||||||
self.device = DeviceApi(IP, None, DISCOVERY_INFO)
|
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(
|
self.device.async_get_wifi_connected_station = AsyncMock(
|
||||||
return_value=CONNECTED_STATIONS
|
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(
|
self.device.async_get_wifi_neighbor_access_points = AsyncMock(
|
||||||
return_value=NEIGHBOR_ACCESS_POINTS
|
return_value=NEIGHBOR_ACCESS_POINTS
|
||||||
)
|
)
|
||||||
|
351
tests/components/devolo_home_network/test_switch.py
Normal file
351
tests/components/devolo_home_network/test_switch.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user