From 2134331e2ba597c5a0de098130105e0df82cf0f1 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 8 Dec 2018 14:49:14 +0100 Subject: [PATCH] Add Philips Moonlight Bedside Lamp support (#18496) * Add Philips Moonlight Bedside Lamp support * Update comment * Make hound happy * Wrap the call that could raise the exception only * Remote blank line * Use updated python-miio API --- homeassistant/components/light/xiaomi_miio.py | 272 ++++++++++++------ 1 file changed, 180 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 9e650562fe8..62433ca9f97 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -1,5 +1,7 @@ """ -Support for Xiaomi Philips Lights (LED Ball & Ceiling Lamp, Eyecare Lamp 2). +Support for Xiaomi Philips Lights. + +LED Ball, Candle, Downlight, Ceiling, Eyecare 2, Bedside & Desklamp Lamp. For more details about this platform, please refer to the documentation https://home-assistant.io/components/light.xiaomi_miio/ @@ -19,7 +21,7 @@ from homeassistant.components.light import ( from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.util import dt +from homeassistant.util import color, dt REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45'] @@ -38,6 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ['philips.light.sread1', 'philips.light.ceiling', 'philips.light.zyceiling', + 'philips.light.moonlight', 'philips.light.bulb', 'philips.light.candle', 'philips.light.candle2', @@ -63,6 +66,13 @@ ATTR_AUTOMATIC_COLOR_TEMPERATURE = 'automatic_color_temperature' ATTR_REMINDER = 'reminder' ATTR_EYECARE_MODE = 'eyecare_mode' +# Moonlight +ATTR_SLEEP_ASSISTANT = 'sleep_assistant' +ATTR_SLEEP_OFF_TIME = 'sleep_off_time' +ATTR_TOTAL_ASSISTANT_SLEEP_TIME = 'total_assistant_sleep_time' +ATTR_BRAND_SLEEP = 'brand_sleep' +ATTR_BRAND = 'brand' + SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off' SERVICE_REMINDER_ON = 'xiaomi_miio_reminder_on' @@ -151,6 +161,12 @@ async def async_setup_platform(hass, config, async_add_entities, device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device + elif model == 'philips.light.moonlight': + from miio import PhilipsMoonlight + light = PhilipsMoonlight(host, token) + device = XiaomiPhilipsMoonlightLamp(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device elif model in ['philips.light.bulb', 'philips.light.candle', 'philips.light.candle2', @@ -307,15 +323,15 @@ class XiaomiPhilipsAbstractLight(Light): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): @@ -335,25 +351,25 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - }) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + }) async def async_set_scene(self, scene: int = 1): """Set the fixed scene.""" @@ -485,29 +501,29 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - self._color_temp = self.translate( - state.color_temperature, - CCT_MIN, CCT_MAX, - self.max_mireds, self.min_mireds) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - }) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + }) @staticmethod def translate(value, left_min, left_max, right_min, right_max): @@ -545,32 +561,32 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - self._color_temp = self.translate( - state.color_temperature, - CCT_MIN, CCT_MAX, - self.max_mireds, self.min_mireds) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, - ATTR_AUTOMATIC_COLOR_TEMPERATURE: - state.automatic_color_temperature, - }) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_AUTOMATIC_COLOR_TEMPERATURE: + state.automatic_color_temperature, + }) class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): @@ -591,28 +607,28 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - ATTR_REMINDER: state.reminder, - ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, - ATTR_EYECARE_MODE: state.eyecare, - }) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_REMINDER: state.reminder, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_EYECARE_MODE: state.eyecare, + }) async def async_set_delayed_turn_off(self, time_period: timedelta): """Set delayed turn off.""" @@ -719,12 +735,84 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.ambient - self._brightness = ceil((255 / 100.0) * state.ambient_brightness) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.ambient + self._brightness = ceil((255 / 100.0) * state.ambient_brightness) + + +class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): + """Representation of a Xiaomi Philips Zhirui Bedside Lamp.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._hs_color = None + self._state_attrs.pop(ATTR_DELAYED_TURN_OFF) + self._state_attrs.update({ + ATTR_SLEEP_ASSISTANT: None, + ATTR_SLEEP_OFF_TIME: None, + ATTR_TOTAL_ASSISTANT_SLEEP_TIME: None, + ATTR_BRAND_SLEEP: None, + ATTR_BRAND: None, + }) + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 153 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 588 + + @property + def hs_color(self) -> tuple: + """Return the hs color value.""" + return self._hs_color + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_executor_job(self._light.status) + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + self._hs_color = color.color_RGB_to_hs(*state.rgb) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_SLEEP_ASSISTANT: state.sleep_assistant, + ATTR_SLEEP_OFF_TIME: state.sleep_off_time, + ATTR_TOTAL_ASSISTANT_SLEEP_TIME: + state.total_assistant_sleep_time, + ATTR_BRAND_SLEEP: state.brand_sleep, + ATTR_BRAND: state.brand, + }) + + async def async_set_delayed_turn_off(self, time_period: timedelta): + """Set delayed turn off. Unsupported.""" + return