diff --git a/.coveragerc b/.coveragerc index d8041b9fe6c..9a03544c1bb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -328,6 +328,7 @@ omit = homeassistant/components/light/tplink.py homeassistant/components/light/tradfri.py homeassistant/components/light/x10.py + homeassistant/components/light/xiaomi_philipslight.py homeassistant/components/light/yeelight.py homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/zengge.py diff --git a/homeassistant/components/light/xiaomi_philipslight.py b/homeassistant/components/light/xiaomi_philipslight.py new file mode 100644 index 00000000000..96d2d7ff9d2 --- /dev/null +++ b/homeassistant/components/light/xiaomi_philipslight.py @@ -0,0 +1,212 @@ +""" +Support for Xiaomi Philips Lights (LED Ball & Ceil). + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/light.xiaomi_philipslight/ +""" +import asyncio +from functools import partial +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.light import ( + PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, + ATTR_COLOR_TEMP, SUPPORT_COLOR_TEMP, Light, ) + +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ) +from homeassistant.exceptions import PlatformNotReady + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Xiaomi Philips Light' +PLATFORM = 'xiaomi_philipslight' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +REQUIREMENTS = ['python-mirobo==0.1.3'] + +# The light does not accept cct values < 1 +CCT_MIN = 1 +CCT_MAX = 100 + +SUCCESS = ['ok'] + + +# pylint: disable=unused-argument +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the light from config.""" + from mirobo import Ceil, DeviceException + if PLATFORM not in hass.data: + hass.data[PLATFORM] = {} + + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + + try: + light = Ceil(host, token) + + philips_light = XiaomiPhilipsLight(name, light) + hass.data[PLATFORM][host] = philips_light + except DeviceException: + raise PlatformNotReady + + async_add_devices([philips_light], update_before_add=True) + + +class XiaomiPhilipsLight(Light): + """Representation of a Xiaomi Philips Light.""" + + def __init__(self, name, light): + """Initialize the light device.""" + self._name = name + + self._brightness = None + self._color_temp = None + + self._light = light + self._state = None + + @property + def should_poll(self): + """Poll the light.""" + return True + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def available(self): + """Return true when state is known.""" + return self._state is not None + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def color_temp(self): + """Return the color temperature.""" + return self._color_temp + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 175 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 333 + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + @asyncio.coroutine + def _try_command(self, mask_error, func, *args, **kwargs): + """Call a light command handling error messages.""" + from mirobo import DeviceException + try: + result = yield from self.hass.async_add_job( + partial(func, *args, **kwargs)) + + _LOGGER.debug("Response received from light: %s", result) + + return result == SUCCESS + except DeviceException as exc: + _LOGGER.error(mask_error, exc) + return False + + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + percent_brightness = int(100 * brightness / 255) + + _LOGGER.debug( + "Setting brightness: %s %s%%", + self.brightness, percent_brightness) + + result = yield from self._try_command( + "Setting brightness failed: %s", + self._light.set_bright, percent_brightness) + + if result: + self._brightness = brightness + + if ATTR_COLOR_TEMP in kwargs: + color_temp = kwargs[ATTR_COLOR_TEMP] + percent_color_temp = self.translate( + color_temp, self.max_mireds, + self.min_mireds, CCT_MIN, CCT_MAX) + + _LOGGER.debug( + "Setting color temperature: " + "%s mireds, %s%% cct", + color_temp, percent_color_temp) + + result = yield from self._try_command( + "Setting color temperature failed: %s cct", + self._light.set_cct, percent_color_temp) + + if result: + self._color_temp = color_temp + + result = yield from self._try_command( + "Turning the light on failed.", self._light.on) + + if result: + self._state = True + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn the light off.""" + result = yield from self._try_command( + "Turning the light off failed.", self._light.off) + + if result: + self._state = True + + @asyncio.coroutine + def async_update(self): + """Fetch state from the device.""" + from mirobo import DeviceException + try: + state = yield from self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state.data) + + self._state = state.is_on + self._brightness = int(255 * 0.01 * state.bright) + self._color_temp = self.translate(state.cct, CCT_MIN, CCT_MAX, + self.max_mireds, + self.min_mireds) + + except DeviceException as ex: + _LOGGER.error("Got exception while fetching the state: %s", ex) + + @staticmethod + def translate(value, left_min, left_max, right_min, right_max): + """Map a value from left span to right span.""" + left_span = left_max - left_min + right_span = right_max - right_min + value_scaled = float(value - left_min) / float(left_span) + return int(right_min + (value_scaled * right_span)) diff --git a/homeassistant/components/vacuum/xiaomi.py b/homeassistant/components/vacuum/xiaomi.py index 5e5081a2aa8..95d7478aa9f 100644 --- a/homeassistant/components/vacuum/xiaomi.py +++ b/homeassistant/components/vacuum/xiaomi.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-mirobo==0.1.2'] +REQUIREMENTS = ['python-mirobo==0.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f1fc40a2187..dc6cd8e7b38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -740,8 +740,9 @@ python-juicenet==0.0.5 # homeassistant.components.lirc # python-lirc==1.2.3 +# homeassistant.components.light.xiaomi_philipslight # homeassistant.components.vacuum.xiaomi -python-mirobo==0.1.2 +python-mirobo==0.1.3 # homeassistant.components.media_player.mpd python-mpd2==0.5.5