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