From eb7742ea7cc90ab9b00d32e138015f1781cd85e1 Mon Sep 17 00:00:00 2001 From: Angelo Gagliano <25516409+TheGardenMonkey@users.noreply.github.com> Date: Thu, 3 Sep 2020 22:05:37 -0400 Subject: [PATCH] Add support for VeSync Fans (#36132) Co-authored-by: J. Nick Koston <nick@koston.org> --- .coveragerc | 1 + CODEOWNERS | 2 +- homeassistant/components/vesync/__init__.py | 44 +++++-- homeassistant/components/vesync/common.py | 13 +- homeassistant/components/vesync/const.py | 1 + homeassistant/components/vesync/fan.py | 117 ++++++++++++++++++ homeassistant/components/vesync/manifest.json | 14 ++- homeassistant/components/vesync/switch.py | 14 ++- 8 files changed, 181 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/vesync/fan.py diff --git a/.coveragerc b/.coveragerc index fdfb25be56b..0730843ccb6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -938,6 +938,7 @@ omit = homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py homeassistant/components/vesync/const.py + homeassistant/components/vesync/fan.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* diff --git a/CODEOWNERS b/CODEOWNERS index ddd36cc7da8..a093bc722be 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -464,7 +464,7 @@ homeassistant/components/velux/* @Julius2342 homeassistant/components/vera/* @vangorra homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff -homeassistant/components/vesync/* @markperdue @webdjoe +homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey homeassistant/components/vicare/* @oischinger homeassistant/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 0f905b8d7ef..94a0d5c2f25 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -1,4 +1,5 @@ -"""Etekcity VeSync integration.""" +"""VeSync integration.""" +import asyncio import logging from pyvesync import VeSync @@ -16,10 +17,13 @@ from .const import ( SERVICE_UPDATE_DEVS, VS_DISCOVERY, VS_DISPATCHERS, + VS_FANS, VS_MANAGER, VS_SWITCHES, ) +PLATFORMS = ["switch", "fan"] + _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( @@ -80,6 +84,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN][VS_MANAGER] = manager switches = hass.data[DOMAIN][VS_SWITCHES] = [] + fans = hass.data[DOMAIN][VS_FANS] = [] hass.data[DOMAIN][VS_DISPATCHERS] = [] @@ -87,13 +92,19 @@ async def async_setup_entry(hass, config_entry): switches.extend(device_dict[VS_SWITCHES]) hass.async_create_task(forward_setup(config_entry, "switch")) + if device_dict[VS_FANS]: + fans.extend(device_dict[VS_FANS]) + hass.async_create_task(forward_setup(config_entry, "fan")) + async def async_new_device_discovery(service): """Discover if new devices should be added.""" manager = hass.data[DOMAIN][VS_MANAGER] switches = hass.data[DOMAIN][VS_SWITCHES] + fans = hass.data[DOMAIN][VS_FANS] dev_dict = await async_process_devices(hass, manager) switch_devs = dev_dict.get(VS_SWITCHES, []) + fan_devs = dev_dict.get(VS_FANS, []) switch_set = set(switch_devs) new_switches = list(switch_set.difference(switches)) @@ -105,6 +116,16 @@ async def async_setup_entry(hass, config_entry): switches.extend(new_switches) hass.async_create_task(forward_setup(config_entry, "switch")) + fan_set = set(fan_devs) + new_fans = list(fan_set.difference(fans)) + if new_fans and fans: + fans.extend(new_fans) + async_dispatcher_send(hass, VS_DISCOVERY.format(VS_FANS), new_fans) + return + if new_fans and not fans: + fans.extend(new_fans) + hass.async_create_task(forward_setup(config_entry, "fan")) + hass.services.async_register( DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery ) @@ -114,14 +135,15 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" - forward_unload = hass.config_entries.async_forward_entry_unload - remove_switches = False - if hass.data[DOMAIN][VS_SWITCHES]: - remove_switches = await forward_unload(entry, "switch") - - if remove_switches: - hass.services.async_remove(DOMAIN, SERVICE_UPDATE_DEVS) - del hass.data[DOMAIN] - return True + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) - return False + return unload_ok diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index d2ffa5281e9..42e3516f085 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -3,7 +3,7 @@ import logging from homeassistant.helpers.entity import ToggleEntity -from .const import VS_SWITCHES +from .const import VS_FANS, VS_SWITCHES _LOGGER = logging.getLogger(__name__) @@ -12,9 +12,14 @@ async def async_process_devices(hass, manager): """Assign devices to proper component.""" devices = {} devices[VS_SWITCHES] = [] + devices[VS_FANS] = [] await hass.async_add_executor_job(manager.update) + if manager.fans: + devices[VS_FANS].extend(manager.fans) + _LOGGER.info("%d VeSync fans found", len(manager.fans)) + if manager.outlets: devices[VS_SWITCHES].extend(manager.outlets) _LOGGER.info("%d VeSync outlets found", len(manager.outlets)) @@ -49,7 +54,7 @@ class VeSyncDevice(ToggleEntity): @property def is_on(self): - """Return True if switch is on.""" + """Return True if device is on.""" return self.device.device_status == "on" @property @@ -57,10 +62,6 @@ class VeSyncDevice(ToggleEntity): """Return True if device is available.""" return self.device.connection_status == "online" - def turn_on(self, **kwargs): - """Turn the device on.""" - self.device.turn_on() - def turn_off(self, **kwargs): """Turn the device off.""" self.device.turn_off() diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index d65edc949c7..9923ab94ecf 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -6,4 +6,5 @@ VS_DISCOVERY = "vesync_discovery_{}" SERVICE_UPDATE_DEVS = "update_devices" VS_SWITCHES = "switches" +VS_FANS = "fans" VS_MANAGER = "manager" diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py new file mode 100644 index 00000000000..7d395d93a74 --- /dev/null +++ b/homeassistant/components/vesync/fan.py @@ -0,0 +1,117 @@ +"""Support for VeSync fans.""" +import logging + +from homeassistant.components.fan import ( + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .common import VeSyncDevice +from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_FANS + +_LOGGER = logging.getLogger(__name__) + +DEV_TYPE_TO_HA = { + "LV-PUR131S": "fan", +} + +SPEED_AUTO = "auto" +FAN_SPEEDS = [SPEED_AUTO, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the VeSync fan platform.""" + + async def async_discover(devices): + """Add new devices to platform.""" + _async_setup_entities(devices, async_add_entities) + + disp = async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_FANS), async_discover) + hass.data[DOMAIN][VS_DISPATCHERS].append(disp) + + _async_setup_entities(hass.data[DOMAIN][VS_FANS], async_add_entities) + return True + + +@callback +def _async_setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + dev_list = [] + for dev in devices: + if DEV_TYPE_TO_HA.get(dev.device_type) == "fan": + dev_list.append(VeSyncFanHA(dev)) + else: + _LOGGER.warning( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + continue + + async_add_entities(dev_list, update_before_add=True) + + +class VeSyncFanHA(VeSyncDevice, FanEntity): + """Representation of a VeSync fan.""" + + def __init__(self, fan): + """Initialize the VeSync fan device.""" + super().__init__(fan) + self.smartfan = fan + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_SET_SPEED + + @property + def speed(self): + """Return the current speed.""" + if self.smartfan.mode == SPEED_AUTO: + return SPEED_AUTO + if self.smartfan.mode == "manual": + current_level = self.smartfan.fan_level + if current_level is not None: + return FAN_SPEEDS[current_level] + return None + + @property + def speed_list(self): + """Get the list of available speeds.""" + return FAN_SPEEDS + + @property + def unique_info(self): + """Return the ID of this fan.""" + return self.smartfan.uuid + + @property + def device_state_attributes(self): + """Return the state attributes of the fan.""" + return { + "mode": self.smartfan.mode, + "active_time": self.smartfan.active_time, + "filter_life": self.smartfan.filter_life, + "air_quality": self.smartfan.air_quality, + "screen_status": self.smartfan.screen_status, + } + + def set_speed(self, speed): + """Set the speed of the device.""" + if not self.smartfan.is_on: + self.smartfan.turn_on() + + if speed is None or speed == SPEED_AUTO: + self.smartfan.auto_mode() + else: + self.smartfan.manual_mode() + self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed)) + + def turn_on(self, speed: str = None, **kwargs) -> None: + """Turn the device on.""" + self.smartfan.turn_on() + self.set_speed(speed) diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 7ac8e89fb60..a4e786c2a12 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -1,8 +1,14 @@ { "domain": "vesync", - "name": "Etekcity VeSync", + "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", - "codeowners": ["@markperdue", "@webdjoe"], - "requirements": ["pyvesync==1.1.0"], + "codeowners": [ + "@markperdue", + "@webdjoe", + "@thegardenmonkey" + ], + "requirements": [ + "pyvesync==1.1.0" + ], "config_flow": true -} +} \ No newline at end of file diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index fb6e83227e9..939240349d1 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,4 +1,4 @@ -"""Support for Etekcity VeSync switches.""" +"""Support for VeSync switches.""" import logging from homeassistant.components.switch import SwitchEntity @@ -55,7 +55,15 @@ def _async_setup_entities(devices, async_add_entities): async_add_entities(dev_list, update_before_add=True) -class VeSyncSwitchHA(VeSyncDevice, SwitchEntity): +class VeSyncBaseSwitch(VeSyncDevice, SwitchEntity): + """Base class for VeSync switch Device Representations.""" + + def turn_on(self, **kwargs): + """Turn the device on.""" + self.device.turn_on() + + +class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): """Representation of a VeSync switch.""" def __init__(self, plug): @@ -90,7 +98,7 @@ class VeSyncSwitchHA(VeSyncDevice, SwitchEntity): self.smartplug.update_energy() -class VeSyncLightSwitch(VeSyncDevice, SwitchEntity): +class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity): """Handle representation of VeSync Light Switch.""" def __init__(self, switch): -- GitLab