diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index c51b6a913d3c1d..07b5d0773a3dcb 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -4,6 +4,8 @@ from pyvesync import VeSync from pyvesync.vesyncbasedevice import VeSyncBaseDevice +from pyvesync.vesyncoutlet import VeSyncOutlet +from pyvesync.vesyncswitch import VeSyncWallSwitch from homeassistant.core import HomeAssistant @@ -32,3 +34,15 @@ def is_humidifier(device: VeSyncBaseDevice) -> bool: """Check if the device represents a humidifier.""" 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) diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index 841185e4308f96..a20e94e22d2d16 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -1,6 +1,11 @@ """Constants for VeSync Component.""" -from pyvesync.vesyncfan import VeSyncHumid200300S, VeSyncSuperior6000S +from pyvesync.vesyncfan import ( + VeSyncAir131, + VeSyncAirBypass, + VeSyncHumid200300S, + VeSyncSuperior6000S, +) DOMAIN = "vesync" VS_DISCOVERY = "vesync_discovery_{}" @@ -32,6 +37,9 @@ VeSyncHumidifierDevice = VeSyncHumid200300S | VeSyncSuperior6000S """Humidifier device types""" +VeSyncFanDevice = VeSyncAirBypass | VeSyncAir131 +"""Humidifier device types""" + DEV_TYPE_TO_HA = { "wifi-switch-1.3": "outlet", "ESW03-USA": "outlet", diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index ef8e6c6051f2a5..f0147db2997725 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,29 +1,59 @@ """Support for VeSync switches.""" +from collections.abc import Callable +from dataclasses import dataclass import logging -from typing import Any +from typing import Any, Final 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.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 .entity import VeSyncBaseEntity _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( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up switches.""" + """Set up switch platform.""" coordinator = hass.data[DOMAIN][VS_COORDINATOR] @@ -45,53 +75,45 @@ def _setup_entities( async_add_entities, coordinator: VeSyncDataCoordinator, ): - """Check if device is a switch and add entity.""" - entities: list[VeSyncBaseSwitch] = [] - for dev in devices: - if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet": - entities.append(VeSyncSwitchHA(dev, coordinator)) - elif DEV_TYPE_TO_HA.get(dev.device_type) == "switch": - entities.append(VeSyncLightSwitch(dev, coordinator)) - - async_add_entities(entities, update_before_add=True) - - -class VeSyncBaseSwitch(VeSyncBaseEntity, SwitchEntity): - """Base class for VeSync switch Device Representations.""" - - _attr_name = None - - def turn_on(self, **kwargs: Any) -> None: - """Turn the device on.""" - self.device.turn_on() - - @property - def is_on(self) -> bool: - """Return True if device is on.""" - return self.device.device_status == "on" + """Check if device is online and add entity.""" + async_add_entities( + VeSyncSwitchEntity(dev, description, coordinator) + for dev in devices + for description in SENSOR_DESCRIPTIONS + if description.exists_fn(dev) + ) - def turn_off(self, **kwargs: Any) -> None: - """Turn the device off.""" - self.device.turn_off() +class VeSyncSwitchEntity(SwitchEntity, VeSyncBaseEntity): + """VeSync switch entity class.""" -class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): - """Representation of a VeSync switch.""" + entity_description: VeSyncSwitchEntityDescription def __init__( - self, plug: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator + self, + device: VeSyncBaseDevice, + description: VeSyncSwitchEntityDescription, + coordinator: VeSyncDataCoordinator, ) -> None: - """Initialize the VeSync switch device.""" - super().__init__(plug, coordinator) - self.smartplug = plug + """Initialize the sensor.""" + super().__init__(device, coordinator) + self.entity_description = description + if is_outlet(self.device): + self._attr_device_class = SwitchDeviceClass.OUTLET + elif is_wall_switch(self.device): + self._attr_device_class = SwitchDeviceClass.SWITCH + @property + def is_on(self) -> bool | None: + """Return the entity value to represent the entity state.""" + return self.entity_description.is_on(self.device) -class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity): - """Handle representation of VeSync Light Switch.""" + def turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self.entity_description.off_fn(self.device) + self.schedule_update_ha_state() - def __init__( - self, switch: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator - ) -> None: - """Initialize Light Switch device class.""" - super().__init__(switch, coordinator) - self.switch = switch + def turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self.entity_description.on_fn(self.device) + self.schedule_update_ha_state() diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 596aa0c94ada65..6464ca5a3e7364 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -360,7 +360,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'vesync', @@ -375,6 +375,7 @@ # name: test_switch_state[Outlet][switch.outlet] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', 'friendly_name': 'Outlet', }), 'context': , @@ -480,7 +481,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'vesync', @@ -495,6 +496,7 @@ # name: test_switch_state[Wall Switch][switch.wall_switch] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'switch', 'friendly_name': 'Wall Switch', }), 'context': ,