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 Devices - Outlets and Switches #24953

Merged
merged 54 commits into from Jul 23, 2019
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
750534e
Change dependency to pyvesync-v2 for vesync switch
webdjoe Feb 20, 2019
bf79e18
async vesync component
webdjoe Jun 21, 2019
b87eaa9
FInish data_entry_flow
webdjoe Jun 25, 2019
ca0fa6d
Update config flow
webdjoe Jun 25, 2019
18b53bb
strings.json
webdjoe Jun 25, 2019
b7e43a8
Minor fix
webdjoe Jun 25, 2019
0e72f65
Syntax fix
webdjoe Jun 25, 2019
7cc787a
Minor Fixs
webdjoe Jun 25, 2019
99a195a
webdjoe Jun 25, 2019
f4d5e57
webdjoe Jun 25, 2019
6c6f592
webdjoe Jun 25, 2019
520cf77
UI Fix
webdjoe Jun 26, 2019
c031fb6
Minor Correct
webdjoe Jun 26, 2019
12e7913
Debug lines
webdjoe Jun 27, 2019
d1d8aa2
fix device dictionaries
webdjoe Jun 27, 2019
6f50cec
Light switch fix
webdjoe Jun 28, 2019
07318ef
Cleanup
webdjoe Jun 30, 2019
cf01c0e
pylint fixes
webdjoe Jul 4, 2019
f3c9ba4
Hassfest and setup scripts
webdjoe Jul 4, 2019
acb6e0b
Flake8 fixes
webdjoe Jul 4, 2019
f71553e
Add vesync light platform
webdjoe Jul 10, 2019
a878809
Fix typo
webdjoe Jul 10, 2019
40a8558
Update Devices Service
webdjoe Jul 13, 2019
868e391
Fix update devices service
webdjoe Jul 13, 2019
efd3d80
Add initial test
webdjoe Jul 13, 2019
a4d5e38
Add Config Flow Tests
webdjoe Jul 14, 2019
53cce3c
Remove Extra Platforms
webdjoe Jul 14, 2019
eaece9d
Fix requirements
webdjoe Jul 15, 2019
e24cd0e
Update pypi package
webdjoe Jul 15, 2019
607a671
Add login to config_flow
webdjoe Jul 15, 2019
4dccacd
Fix variable import
webdjoe Jul 15, 2019
871e9e9
Update config_flow.py
webdjoe Jul 15, 2019
859b2d8
Update config_flow.py
webdjoe Jul 15, 2019
23a8498
Put VS object into hass.data instead of config entry
webdjoe Jul 15, 2019
4be6c38
Update __init__.py
webdjoe Jul 15, 2019
fe42326
Handle Login Error
webdjoe Jul 15, 2019
fde55b2
Fix invalid login error
webdjoe Jul 15, 2019
87c8bd0
Fix typo
webdjoe Jul 15, 2019
d33049d
Remove line
webdjoe Jul 15, 2019
a9fec2c
PEP fixes
webdjoe Jul 15, 2019
8d8a5df
Fix change requests
webdjoe Jul 17, 2019
7e2b23e
Fix typo
webdjoe Jul 17, 2019
aca9963
Update __init__.py
webdjoe Jul 17, 2019
caa1136
Update switch.py
webdjoe Jul 17, 2019
d70bd46
Flake8 fix
webdjoe Jul 17, 2019
f2ecb31
Update test requirements
webdjoe Jul 17, 2019
2611663
Fix permission
webdjoe Jul 17, 2019
18f25d1
Merge branch 'dev' of https://github.com/webdjoe/home-assistant into dev
webdjoe Jul 17, 2019
ec32214
Address change requests
webdjoe Jul 17, 2019
ef70152
Address change requests
webdjoe Jul 18, 2019
133008e
Fix device discovery indent, add MockConfigEntry
webdjoe Jul 19, 2019
985548b
Fix vesynclightswitch classs
webdjoe Jul 21, 2019
dcd6634
Remove active time attribute
webdjoe Jul 21, 2019
15b8015
Remove time_zone, grammar check
webdjoe Jul 21, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Expand Up @@ -667,6 +667,9 @@ omit =
homeassistant/components/venstar/climate.py
homeassistant/components/vera/*
homeassistant/components/verisure/*
homeassistant/components/vesync/__init__.py
homeassistant/components/vesync/common.py
homeassistant/components/vesync/const.py
homeassistant/components/vesync/switch.py
homeassistant/components/viaggiatreno/sensor.py
homeassistant/components/vizio/media_player.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -280,6 +280,7 @@ homeassistant/components/uptimerobot/* @ludeeus
homeassistant/components/utility_meter/* @dgomes
homeassistant/components/velux/* @Julius2342
homeassistant/components/version/* @fabaff
homeassistant/components/vesync/* @markperdue @webdjoe
homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf
homeassistant/components/waqi/* @andrey-git
Expand Down
20 changes: 20 additions & 0 deletions homeassistant/components/vesync/.translations/en.json
@@ -0,0 +1,20 @@
{
"config": {
"title": "VeSync",
"step": {
"user": {
"title": "Enter Username and Password",
"data": {
"username": "Email Address",
"password": "Password"
}
}
},
"error": {
"invalid_login": "Invalid username or password"
},
"abort": {
"already_setup": "Only one Vesync instance is allow"
}
}
}
115 changes: 114 additions & 1 deletion homeassistant/components/vesync/__init__.py
@@ -1 +1,114 @@
"""The vesync component."""
"""Etekcity VeSync integration."""
import logging
import voluptuous as vol
from pyvesync import VeSync
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.config_entries import SOURCE_IMPORT
from .common import async_process_devices
from .config_flow import configured_instances
from .const import (DOMAIN, VS_DISPATCHERS, VS_DISCOVERY, VS_SWITCHES,
SERVICE_UPDATE_DEVS, VS_MANAGER)

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)


async def async_setup(hass, config):
"""Set up the VeSync component."""
conf = config.get(DOMAIN)

if conf is None:
return True

if not configured_instances(hass):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': SOURCE_IMPORT},
data={
CONF_USERNAME: conf[CONF_USERNAME],
CONF_PASSWORD: conf[CONF_PASSWORD]
}))

return True


async def async_setup_entry(hass, config_entry):
"""Set up Vesync as config entry."""
username = config_entry.data[CONF_USERNAME]
password = config_entry.data[CONF_PASSWORD]

webdjoe marked this conversation as resolved.
Show resolved Hide resolved
time_zone = str(hass.config.time_zone)

manager = VeSync(username, password, time_zone)

login = await hass.async_add_executor_job(manager.login)

if not login:
_LOGGER.error("Unable to login to the VeSync server")
return False

device_dict = await async_process_devices(hass, manager)

forward_setup = hass.config_entries.async_forward_entry_setup

hass.data[DOMAIN] = {}
hass.data[DOMAIN][VS_MANAGER] = manager

switches = hass.data[DOMAIN][VS_SWITCHES] = []

hass.data[DOMAIN][VS_DISPATCHERS] = []

if device_dict[VS_SWITCHES]:
switches.extend(device_dict[VS_SWITCHES])
hass.async_create_task(forward_setup(config_entry, 'switch'))

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]

dev_dict = await async_process_devices(hass, manager)
switch_devs = dev_dict.get(VS_SWITCHES, [])

switch_set = set(switch_devs)
new_switches = list(switch_set.difference(switches))
if new_switches and switches:
switches.extend(new_switches)
async_dispatcher_send(hass,
VS_DISCOVERY.format(VS_SWITCHES),
new_switches)
return
if new_switches and not switches:
switches.extend(new_switches)
hass.async_create_task(forward_setup(config_entry, 'switch'))

hass.services.async_register(DOMAIN,
SERVICE_UPDATE_DEVS,
async_new_device_discovery
)

return True


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

return False
70 changes: 70 additions & 0 deletions homeassistant/components/vesync/common.py
@@ -0,0 +1,70 @@
"""Common utilities for VeSync Component."""
import logging
from homeassistant.helpers.entity import ToggleEntity
from .const import VS_SWITCHES

_LOGGER = logging.getLogger(__name__)


async def async_process_devices(hass, manager):
"""Assign devices to proper component."""
devices = {}
devices[VS_SWITCHES] = []

await hass.async_add_executor_job(manager.update)

if manager.outlets:
devices[VS_SWITCHES].extend(manager.outlets)
_LOGGER.info("%d VeSync outlets found", len(manager.outlets))

if manager.switches:
for switch in manager.switches:
if not switch.is_dimmable():
devices[VS_SWITCHES].append(switch)
_LOGGER.info(
"%d VeSync standard switches found", len(manager.switches))

return devices


class VeSyncDevice(ToggleEntity):
"""Base class for VeSync Device Representations."""

def __init__(self, device):
"""Initialize the VeSync device."""
self.device = device

@property
def unique_id(self):
"""Return the ID of this device."""
if isinstance(self.device.sub_device_no, int):
return ('{}{}'.format(
self.device.cid, str(self.device.sub_device_no)))
return self.device.cid

@property
def name(self):
"""Return the name of the device."""
return self.device.device_name

@property
def is_on(self):
"""Return True if switch 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()

def update(self):
"""Update vesync device."""
self.device.update()
71 changes: 71 additions & 0 deletions homeassistant/components/vesync/config_flow.py
@@ -0,0 +1,71 @@
"""Config flow utilities."""
import logging
from collections import OrderedDict
import voluptuous as vol
from pyvesync import VeSync
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


@callback
def configured_instances(hass):
"""Return already configured instances."""
return hass.config_entries.async_entries(DOMAIN)


@config_entries.HANDLERS.register(DOMAIN)
class VeSyncFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

def __init__(self):
"""Instantiate config flow."""
self._username = None
self._password = None
self._time_zone = None
webdjoe marked this conversation as resolved.
Show resolved Hide resolved
self.data_schema = OrderedDict()
self.data_schema[vol.Required(CONF_USERNAME)] = str
self.data_schema[vol.Required(CONF_PASSWORD)] = str

@callback
def _show_form(self, errors=None):
"""Show form to the user."""
return self.async_show_form(
step_id='user',
data_schema=vol.Schema(self.data_schema),
errors=errors if errors else {},
)

async def async_step_import(self, import_config):
"""Handle external yaml configuration."""
return await self.async_step_user(import_config)

async def async_step_user(self, user_input=None):
"""Handle a flow start."""
if configured_instances(self.hass):
webdjoe marked this conversation as resolved.
Show resolved Hide resolved
return self.async_abort(reason='already_setup')

if not user_input:
return self._show_form()

self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]

webdjoe marked this conversation as resolved.
Show resolved Hide resolved
manager = VeSync(self._username, self._password)
login = await self.hass.async_add_executor_job(manager.login)
if not login:
return self._show_form(errors={'base': 'invalid_login'})

return self.async_create_entry(
title=self._username,
data={
CONF_USERNAME: self._username,
CONF_PASSWORD: self._password,
},
)
9 changes: 9 additions & 0 deletions homeassistant/components/vesync/const.py
@@ -0,0 +1,9 @@
"""Constants for VeSync Component."""

DOMAIN = 'vesync'
VS_DISPATCHERS = 'vesync_dispatchers'
VS_DISCOVERY = 'vesync_discovery_{}'
SERVICE_UPDATE_DEVS = 'update_devices'

VS_SWITCHES = 'switches'
VS_MANAGER = 'manager'
9 changes: 4 additions & 5 deletions homeassistant/components/vesync/manifest.json
@@ -1,10 +1,9 @@
{
"domain": "vesync",
"name": "Vesync",
"name": "VeSync",
"documentation": "https://www.home-assistant.io/components/vesync",
"requirements": [
"pyvesync_v2==0.9.7"
],
"dependencies": [],
"codeowners": []
"codeowners": ["@markperdue", "@webdjoe"],
"requirements": ["pyvesync==1.1.0"],
"config_flow": true
}
2 changes: 2 additions & 0 deletions homeassistant/components/vesync/services.yaml
@@ -0,0 +1,2 @@
update_devices:
description: Add new VeSync devices to Home Assistant
20 changes: 20 additions & 0 deletions homeassistant/components/vesync/strings.json
@@ -0,0 +1,20 @@
{
"config": {
"title": "VeSync",
"step": {
"user": {
"title": "Enter Username and Password",
"data": {
"username": "Email Address",
"password": "Password"
}
}
},
"error": {
"invalid_login": "Invalid username or password"
},
"abort": {
"already_setup": "Only one Vesync instance is allow"
webdjoe marked this conversation as resolved.
Show resolved Hide resolved
}
}
}