From 8d48164f25f3b7f272ee486ecdeb6e1e8e4c6174 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Tue, 10 Apr 2018 18:38:23 -0700 Subject: [PATCH] Add support for Eufy bulbs and switches (#13773) * Add support for Eufy bulbs and switches Add support for driving bulbs and switches from the Eufy range. * Fix hound checks * Satisfy pylint * Handle review comments * Review updates and test fixes * PyLint is a bit too aggressive --- .coveragerc | 3 + homeassistant/components/eufy.py | 77 ++++++++++++ homeassistant/components/light/eufy.py | 158 ++++++++++++++++++++++++ homeassistant/components/switch/eufy.py | 73 +++++++++++ requirements_all.txt | 3 + 5 files changed, 314 insertions(+) create mode 100644 homeassistant/components/eufy.py create mode 100644 homeassistant/components/light/eufy.py create mode 100644 homeassistant/components/switch/eufy.py diff --git a/.coveragerc b/.coveragerc index 6b1ca91a574..666134488fe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,6 +94,9 @@ omit = homeassistant/components/envisalink.py homeassistant/components/*/envisalink.py + homeassistant/components/eufy.py + homeassistant/components/*/eufy.py + homeassistant/components/gc100.py homeassistant/components/*/gc100.py diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py new file mode 100644 index 00000000000..53584be9fdc --- /dev/null +++ b/homeassistant/components/eufy.py @@ -0,0 +1,77 @@ +""" +Support for Eufy devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/eufy/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, \ + CONF_DEVICES, CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, CONF_NAME +from homeassistant.helpers import discovery + +import homeassistant.helpers.config_validation as cv + + +REQUIREMENTS = ['lakeside==0.4'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'eufy' + +DEVICE_SCHEMA = vol.Schema({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_TYPE): cv.string, + vol.Optional(CONF_NAME): cv.string +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, + [DEVICE_SCHEMA]), + vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +EUFY_DISPATCH = { + 'T1011': 'light', + 'T1012': 'light', + 'T1013': 'light', + 'T1201': 'switch', + 'T1202': 'switch', + 'T1211': 'switch' +} + + +def setup(hass, config): + """Set up Eufy devices.""" + # pylint: disable=import-error + import lakeside + + if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: + data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD]) + for device in data: + kind = device['type'] + if kind not in EUFY_DISPATCH: + continue + discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, + config) + + for device_info in config[DOMAIN][CONF_DEVICES]: + kind = device_info['type'] + if kind not in EUFY_DISPATCH: + continue + device = {} + device['address'] = device_info['address'] + device['code'] = device_info['access_token'] + device['type'] = device_info['type'] + device['name'] = device_info['name'] + discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, + config) + + return True diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/light/eufy.py new file mode 100644 index 00000000000..fa6550d2682 --- /dev/null +++ b/homeassistant/components/light/eufy.py @@ -0,0 +1,158 @@ +""" +Support for Eufy lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.eufy/ +""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) + +import homeassistant.util.color as color_util + +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin as mired_to_kelvin, + color_temperature_kelvin_to_mired as kelvin_to_mired) + +DEPENDENCIES = ['eufy'] + +_LOGGER = logging.getLogger(__name__) + +EUFY_MAX_KELVIN = 6500 +EUFY_MIN_KELVIN = 2700 + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Eufy bulbs.""" + if discovery_info is None: + return + add_devices([EufyLight(discovery_info)], True) + + +class EufyLight(Light): + """Representation of a Eufy light.""" + + def __init__(self, device): + """Initialize the light.""" + # pylint: disable=import-error + import lakeside + + self._temp = None + self._brightness = None + self._hs = None + self._state = None + self._name = device['name'] + self._address = device['address'] + self._code = device['code'] + self._type = device['type'] + self._bulb = lakeside.bulb(self._address, self._code, self._type) + if self._type == "T1011": + self._features = SUPPORT_BRIGHTNESS + elif self._type == "T1012": + self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + elif self._type == "T1013": + self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR + self._bulb.connect() + + def update(self): + """Synchronise state from the bulb.""" + self._bulb.update() + self._brightness = self._bulb.brightness + self._temp = self._bulb.temperature + if self._bulb.colors: + self._hs = color_util.color_RGB_to_hsv(*self._bulb.colors) + else: + self._hs = None + self._state = self._bulb.power + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._address + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(self._brightness * 255 / 100) + + @property + def min_mireds(self): + """Return minimum supported color temperature.""" + return kelvin_to_mired(EUFY_MAX_KELVIN) + + @property + def max_mireds(self): + """Return maximu supported color temperature.""" + return kelvin_to_mired(EUFY_MIN_KELVIN) + + @property + def color_temp(self): + """Return the color temperature of this light.""" + temp_in_k = int(EUFY_MIN_KELVIN + (self._temp * + (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN) + / 100)) + return kelvin_to_mired(temp_in_k) + + @property + def hs_color(self): + """Return the color of this light.""" + return self._hs + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + def turn_on(self, **kwargs): + """Turn the specified light on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + colortemp = kwargs.get(ATTR_COLOR_TEMP) + # pylint: disable=invalid-name + hs = kwargs.get(ATTR_HS_COLOR) + + if brightness is not None: + brightness = int(brightness * 100 / 255) + else: + brightness = max(1, self._brightness) + + if colortemp is not None: + temp_in_k = mired_to_kelvin(colortemp) + relative_temp = temp_in_k - EUFY_MIN_KELVIN + temp = int(relative_temp * 100 / + (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)) + else: + temp = None + + if hs is not None: + rgb = color_util.color_hsv_to_RGB( + hs[0], hs[1], brightness / 255 * 100) + else: + rgb = None + + try: + self._bulb.set_state(power=True, brightness=brightness, + temperature=temp, colors=rgb) + except BrokenPipeError: + self._bulb.connect() + self._bulb.set_state(power=True, brightness=brightness, + temperature=temp, colors=rgb) + + def turn_off(self, **kwargs): + """Turn the specified light off.""" + try: + self._bulb.set_state(power=False) + except BrokenPipeError: + self._bulb.connect() + self._bulb.set_state(power=False) diff --git a/homeassistant/components/switch/eufy.py b/homeassistant/components/switch/eufy.py new file mode 100644 index 00000000000..891525d3979 --- /dev/null +++ b/homeassistant/components/switch/eufy.py @@ -0,0 +1,73 @@ +""" +Support for Eufy switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.eufy/ +""" +import logging + +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = ['eufy'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Eufy switches.""" + if discovery_info is None: + return + add_devices([EufySwitch(discovery_info)], True) + + +class EufySwitch(SwitchDevice): + """Representation of a Eufy switch.""" + + def __init__(self, device): + """Initialize the light.""" + # pylint: disable=import-error + import lakeside + + self._state = None + self._name = device['name'] + self._address = device['address'] + self._code = device['code'] + self._type = device['type'] + self._switch = lakeside.switch(self._address, self._code, self._type) + self._switch.connect() + + def update(self): + """Synchronise state from the switch.""" + self._switch.update() + self._state = self._switch.power + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._address + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the specified switch on.""" + try: + self._switch.set_state(True) + except BrokenPipeError: + self._switch.connect() + self._switch.set_state(power=True) + + def turn_off(self, **kwargs): + """Turn the specified switch off.""" + try: + self._switch.set_state(False) + except BrokenPipeError: + self._switch.connect() + self._switch.set_state(False) diff --git a/requirements_all.txt b/requirements_all.txt index d5d64381528..1b3d3206c60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -454,6 +454,9 @@ keyring==12.0.0 # homeassistant.scripts.keyring keyrings.alt==3.0 +# homeassistant.components.eufy +lakeside==0.4 + # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http libnacl==1.6.1