Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for VeSync Fans #36132

Merged
merged 16 commits into from
Sep 4, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,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/*
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,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
Expand Down
44 changes: 33 additions & 11 deletions homeassistant/components/vesync/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Etekcity VeSync integration."""
"""VeSync integration."""
import asyncio
import logging

from pyvesync import VeSync
Expand All @@ -16,10 +17,13 @@
SERVICE_UPDATE_DEVS,
VS_DISCOVERY,
VS_DISPATCHERS,
VS_FANS,
VS_MANAGER,
VS_SWITCHES,
)

PLATFORMS = ["switch", "fan"]

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
Expand Down Expand Up @@ -80,20 +84,27 @@ 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] = []

bdraco marked this conversation as resolved.
Show resolved Hide resolved
if device_dict[VS_SWITCHES]:
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))
Expand All @@ -105,6 +116,16 @@ async def async_new_device_discovery(service):
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
)
Expand All @@ -114,14 +135,15 @@ async def async_new_device_discovery(service):

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
13 changes: 7 additions & 6 deletions homeassistant/components/vesync/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from homeassistant.helpers.entity import ToggleEntity

from .const import VS_SWITCHES
from .const import VS_FANS, VS_SWITCHES

_LOGGER = logging.getLogger(__name__)

Expand All @@ -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))
Expand Down Expand Up @@ -49,18 +54,14 @@ def name(self):

@property
def is_on(self):
"""Return True if switch is on."""
"""Return True if device is on."""
return self.device.device_status == "on"

@property
def available(self) -> bool:
"""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()
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/vesync/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
SERVICE_UPDATE_DEVS = "update_devices"

VS_SWITCHES = "switches"
VS_FANS = "fans"
VS_MANAGER = "manager"
114 changes: 114 additions & 0 deletions homeassistant/components/vesync/fan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""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]
bdraco marked this conversation as resolved.
Show resolved Hide resolved


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the VeSync fan platform."""

async def async_discover(devices):
Copy link
Member

@MartinHjelmare MartinHjelmare Sep 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async_discover could be a callback since we don't await inside.

"""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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing is checking this return value.



@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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the fan can still return SPEED_AUTO, and we have taken it out of FAN_SPEEDS this puts us in an invalid state. Unfortunately it doesn't look like we can tell what speed the fan is actually running it so it leaves us in a position where violating the spec might make more sense since auto can be set from outside Home Assistant.

I think its ok to add back in as long as we refactor after home-assistant/architecture#127 resolves this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good and yes I will refactor when a decision is made.

Copy link
Member

@MartinHjelmare MartinHjelmare Sep 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not ok to violate our architecture design until the architecture has been changed. Please remove the auto speed.

We can return None in that case.

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Air quality should be a separate sensor entity if it means a measurement of air quality and not a fan setting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'fan' reports an air quality which ranges between bad to excellent, so just want to clarify if that makes sense and is still OK to keep as a sensor. I am assuming yes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to make it a sensor in this case 👍

"screen_status": self.smartfan.screen_status,
}

def set_speed(self, speed):
"""Set the speed of the device."""
if speed is None or speed == SPEED_AUTO:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this case.

self.smartfan.auto_mode()
else:
self.smartfan.manual_mode()
self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed))

bdraco marked this conversation as resolved.
Show resolved Hide resolved
def turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the device on."""
self.smartfan.turn_on()
self.set_speed(speed)
14 changes: 10 additions & 4 deletions homeassistant/components/vesync/manifest.json
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 9 additions & 1 deletion homeassistant/components/vesync/switch.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Support for Etekcity VeSync switches."""
"""Support for VeSync switches."""
import logging

from homeassistant.components.switch import SwitchEntity
Expand Down Expand Up @@ -89,6 +89,10 @@ def update(self):
self.smartplug.update()
self.smartplug.update_energy()

def turn_on(self, **kwargs):
TheGardenMonkey marked this conversation as resolved.
Show resolved Hide resolved
"""Turn the device on."""
self.device.turn_on()


class VeSyncLightSwitch(VeSyncDevice, SwitchEntity):
"""Handle representation of VeSync Light Switch."""
Expand All @@ -97,3 +101,7 @@ def __init__(self, switch):
"""Initialize Light Switch device class."""
super().__init__(switch)
self.switch = switch

def turn_on(self, **kwargs):
"""Turn the device on."""
self.device.turn_on()