Refactor switch for vesync (#134409)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
cdnninja 2025-02-05 09:07:35 -07:00 committed by GitHub
parent 078996effd
commit 4694240cfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 49 deletions

View File

@ -4,6 +4,8 @@ import logging
from pyvesync import VeSync from pyvesync import VeSync
from pyvesync.vesyncbasedevice import VeSyncBaseDevice from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from pyvesync.vesyncoutlet import VeSyncOutlet
from pyvesync.vesyncswitch import VeSyncWallSwitch
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -54,3 +56,15 @@ def is_humidifier(device: VeSyncBaseDevice) -> bool:
"""Check if the device represents a humidifier.""" """Check if the device represents a humidifier."""
return isinstance(device, VeSyncHumidifierDevice) return isinstance(device, VeSyncHumidifierDevice)
def is_outlet(device: VeSyncBaseDevice) -> bool:
"""Check if the device represents an outlet."""
return isinstance(device, VeSyncOutlet)
def is_wall_switch(device: VeSyncBaseDevice) -> bool:
"""Check if the device represents a wall switch, note this doessn't include dimming switches."""
return isinstance(device, VeSyncWallSwitch)

View File

@ -1,29 +1,59 @@
"""Support for VeSync switches.""" """Support for VeSync switches."""
from collections.abc import Callable
from dataclasses import dataclass
import logging import logging
from typing import Any from typing import Any, Final
from pyvesync.vesyncbasedevice import VeSyncBaseDevice from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_COORDINATOR, VS_DEVICES, VS_DISCOVERY from .common import is_outlet, is_wall_switch
from .const import DOMAIN, VS_COORDINATOR, VS_DEVICES, VS_DISCOVERY
from .coordinator import VeSyncDataCoordinator from .coordinator import VeSyncDataCoordinator
from .entity import VeSyncBaseEntity from .entity import VeSyncBaseEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class VeSyncSwitchEntityDescription(SwitchEntityDescription):
"""A class that describes custom switch entities."""
is_on: Callable[[VeSyncBaseDevice], bool]
exists_fn: Callable[[VeSyncBaseDevice], bool]
on_fn: Callable[[VeSyncBaseDevice], bool]
off_fn: Callable[[VeSyncBaseDevice], bool]
SENSOR_DESCRIPTIONS: Final[tuple[VeSyncSwitchEntityDescription, ...]] = (
VeSyncSwitchEntityDescription(
key="device_status",
is_on=lambda device: device.device_status == "on",
# Other types of wall switches support dimming. Those use light.py platform.
exists_fn=lambda device: is_wall_switch(device) or is_outlet(device),
name=None,
on_fn=lambda device: device.turn_on(),
off_fn=lambda device: device.turn_off(),
),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up switches.""" """Set up switch platform."""
coordinator = hass.data[DOMAIN][VS_COORDINATOR] coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@ -45,55 +75,46 @@ def _setup_entities(
async_add_entities, async_add_entities,
coordinator: VeSyncDataCoordinator, coordinator: VeSyncDataCoordinator,
): ):
"""Check if device is a switch and add entity.""" """Check if device is online and add entity."""
entities: list[VeSyncBaseSwitch] = [] async_add_entities(
for dev in devices: VeSyncSwitchEntity(dev, description, coordinator)
if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet": for dev in devices
entities.append(VeSyncSwitchHA(dev, coordinator)) for description in SENSOR_DESCRIPTIONS
elif DEV_TYPE_TO_HA.get(dev.device_type) == "switch": if description.exists_fn(dev)
entities.append(VeSyncLightSwitch(dev, coordinator)) )
async_add_entities(entities, update_before_add=True)
class VeSyncBaseSwitch(VeSyncBaseEntity, SwitchEntity): class VeSyncSwitchEntity(SwitchEntity, VeSyncBaseEntity):
"""Base class for VeSync switch Device Representations.""" """VeSync switch entity class."""
_attr_name = None entity_description: VeSyncSwitchEntityDescription
def turn_on(self, **kwargs: Any) -> None: def __init__(
"""Turn the device on.""" self,
self.device.turn_on() device: VeSyncBaseDevice,
description: VeSyncSwitchEntityDescription,
coordinator: VeSyncDataCoordinator,
) -> None:
"""Initialize the sensor."""
super().__init__(device, coordinator)
self.entity_description = description
self._attr_unique_id = f"{super().unique_id}-{description.key}"
if is_outlet(self.device):
self._attr_device_class = SwitchDeviceClass.OUTLET
elif is_wall_switch(self.device):
self._attr_device_class = SwitchDeviceClass.SWITCH
@property @property
def is_on(self) -> bool: def is_on(self) -> bool | None:
"""Return True if device is on.""" """Return the entity value to represent the entity state."""
return self.device.device_status == "on" return self.entity_description.is_on(self.device)
def turn_off(self, **kwargs: Any) -> None: def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the entity off."""
self.device.turn_off() if self.entity_description.off_fn(self.device):
self.schedule_update_ha_state()
def turn_on(self, **kwargs: Any) -> None:
class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): """Turn the entity on."""
"""Representation of a VeSync switch.""" if self.entity_description.on_fn(self.device):
self.schedule_update_ha_state()
def __init__(
self, plug: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator
) -> None:
"""Initialize the VeSync switch device."""
super().__init__(plug, coordinator)
self._attr_unique_id = f"{super().unique_id}-device_status"
self.smartplug = plug
class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity):
"""Handle representation of VeSync Light Switch."""
def __init__(
self, switch: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator
) -> None:
"""Initialize Light Switch device class."""
super().__init__(switch, coordinator)
self._attr_unique_id = f"{super().unique_id}-device_status"
self.switch = switch

View File

@ -360,7 +360,7 @@
'name': None, 'name': None,
'options': dict({ 'options': dict({
}), }),
'original_device_class': None, 'original_device_class': <SwitchDeviceClass.OUTLET: 'outlet'>,
'original_icon': None, 'original_icon': None,
'original_name': None, 'original_name': None,
'platform': 'vesync', 'platform': 'vesync',
@ -375,6 +375,7 @@
# name: test_switch_state[Outlet][switch.outlet] # name: test_switch_state[Outlet][switch.outlet]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'outlet',
'friendly_name': 'Outlet', 'friendly_name': 'Outlet',
}), }),
'context': <ANY>, 'context': <ANY>,
@ -518,7 +519,7 @@
'name': None, 'name': None,
'options': dict({ 'options': dict({
}), }),
'original_device_class': None, 'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None, 'original_icon': None,
'original_name': None, 'original_name': None,
'platform': 'vesync', 'platform': 'vesync',
@ -533,6 +534,7 @@
# name: test_switch_state[Wall Switch][switch.wall_switch] # name: test_switch_state[Wall Switch][switch.wall_switch]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Wall Switch', 'friendly_name': 'Wall Switch',
}), }),
'context': <ANY>, 'context': <ANY>,