From 9482a6303da196c55739af2772896d2e8819913c Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Sun, 20 Jan 2019 19:23:36 -0800 Subject: [PATCH] Add EverLights light component (#19817) * EverLights light integration. Supports single color (with color and brightness parameters) or saved pattern (with effect parameter). * Fix pylint parameter name warning. * Code review feedback. * Add tests for the two helper functions of EverLights component. * Fixes for review feedback. * Change test style. * Style fixes for hound. --- .coveragerc | 1 + homeassistant/components/light/everlights.py | 177 +++++++++++++++++++ requirements_all.txt | 3 + tests/components/light/test_everlights.py | 16 ++ 4 files changed, 197 insertions(+) create mode 100644 homeassistant/components/light/everlights.py create mode 100644 tests/components/light/test_everlights.py diff --git a/.coveragerc b/.coveragerc index 29a9caa8066..93c9b28e103 100644 --- a/.coveragerc +++ b/.coveragerc @@ -581,6 +581,7 @@ omit = homeassistant/components/light/blinkt.py homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora.py + homeassistant/components/light/everlights.py homeassistant/components/light/flux_led.py homeassistant/components/light/futurenow.py homeassistant/components/light/greenwave.py diff --git a/homeassistant/components/light/everlights.py b/homeassistant/components/light/everlights.py new file mode 100644 index 00000000000..31e72c78fd6 --- /dev/null +++ b/homeassistant/components/light/everlights.py @@ -0,0 +1,177 @@ +""" +Support for EverLights lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.everlights/ +""" +import logging +from datetime import timedelta +from typing import Tuple + +import voluptuous as vol + +from homeassistant.const import CONF_HOSTS +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, + SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_COLOR, + Light, PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.exceptions import PlatformNotReady + +REQUIREMENTS = ['pyeverlights==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_EVERLIGHTS = (SUPPORT_EFFECT | SUPPORT_BRIGHTNESS | SUPPORT_COLOR) + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]), +}) + +NAME_FORMAT = "EverLights {} Zone {}" + + +def color_rgb_to_int(red: int, green: int, blue: int) -> int: + """Return a RGB color as an integer.""" + return red*256*256+green*256+blue + + +def color_int_to_rgb(value: int) -> Tuple[int, int, int]: + """Return an RGB tuple from an integer.""" + return (value >> 16, (value >> 8) & 0xff, value & 0xff) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the EverLights lights from configuration.yaml.""" + import pyeverlights + lights = [] + + for ipaddr in config[CONF_HOSTS]: + api = pyeverlights.EverLights(ipaddr, + async_get_clientsession(hass)) + + try: + status = await api.get_status() + + effects = await api.get_all_patterns() + + except pyeverlights.ConnectionError: + raise PlatformNotReady + + else: + lights.append(EverLightsLight(api, pyeverlights.ZONE_1, + status, effects)) + lights.append(EverLightsLight(api, pyeverlights.ZONE_2, + status, effects)) + + async_add_entities(lights) + + +class EverLightsLight(Light): + """Representation of a Flux light.""" + + def __init__(self, api, channel, status, effects): + """Initialize the light.""" + self._api = api + self._channel = channel + self._status = status + self._effects = effects + self._mac = status['mac'] + self._error_reported = False + self._hs_color = [255, 255] + self._brightness = 255 + self._effect = None + self._available = True + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return '{}-{}'.format(self._mac, self._channel) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def name(self): + """Return the name of the device.""" + return NAME_FORMAT.format(self._mac, self._channel) + + @property + def is_on(self): + """Return true if device is on.""" + return self._status['ch{}Active'.format(self._channel)] == 1 + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def hs_color(self): + """Return the color property.""" + return self._hs_color + + @property + def effect(self): + """Return the effect property.""" + return self._effect + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_EVERLIGHTS + + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effects + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + hs_color = kwargs.get(ATTR_HS_COLOR, self._hs_color) + brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) + effect = kwargs.get(ATTR_EFFECT) + + if effect is not None: + colors = await self._api.set_pattern_by_id(self._channel, effect) + + rgb = color_int_to_rgb(colors[0]) + hsv = color_util.color_RGB_to_hsv(*rgb) + hs_color = hsv[:2] + brightness = hsv[2] / 100 * 255 + + else: + rgb = color_util.color_hsv_to_RGB(*hs_color, brightness/255*100) + colors = [color_rgb_to_int(*rgb)] + + await self._api.set_pattern(self._channel, colors) + + self._hs_color = hs_color + self._brightness = brightness + self._effect = effect + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._api.clear_pattern(self._channel) + + async def async_update(self): + """Synchronize state with control box.""" + import pyeverlights + + try: + self._status = await self._api.get_status() + except pyeverlights.ConnectionError: + if self._available: + _LOGGER.warning("EverLights control box connection lost.") + self._available = False + else: + if not self._available: + _LOGGER.warning("EverLights control box connection restored.") + self._available = True diff --git a/requirements_all.txt b/requirements_all.txt index 0e5ccd36625..a7ccfc7fe71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -981,6 +981,9 @@ pyenvisalink==3.8 # homeassistant.components.climate.ephember pyephember==0.2.0 +# homeassistant.components.light.everlights +pyeverlights==0.1.0 + # homeassistant.components.sensor.fido pyfido==2.1.1 diff --git a/tests/components/light/test_everlights.py b/tests/components/light/test_everlights.py new file mode 100644 index 00000000000..026e7927c8d --- /dev/null +++ b/tests/components/light/test_everlights.py @@ -0,0 +1,16 @@ +"""The tests for the everlights component.""" +from homeassistant.components.light import everlights + + +def test_color_rgb_to_int(): + """Test RGB to integer conversion.""" + assert everlights.color_rgb_to_int(0x00, 0x00, 0x00) == 0x000000 + assert everlights.color_rgb_to_int(0xff, 0xff, 0xff) == 0xffffff + assert everlights.color_rgb_to_int(0x12, 0x34, 0x56) == 0x123456 + + +def test_int_to_rgb(): + """Test integer to RGB conversion.""" + assert everlights.color_int_to_rgb(0x000000) == (0x00, 0x00, 0x00) + assert everlights.color_int_to_rgb(0xffffff) == (0xff, 0xff, 0xff) + assert everlights.color_int_to_rgb(0x123456) == (0x12, 0x34, 0x56)