diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 9b9778fd5d28b..a6c9e81e98f4e 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -13,7 +13,8 @@ from homeassistant.helpers import discovery from homeassistant.helpers.discovery import load_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import dispatcher_send, \ + dispatcher_connect from homeassistant.helpers.event import track_time_interval REQUIREMENTS = ['yeelight==0.4.4'] @@ -23,6 +24,7 @@ DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = 'yeelight_{}_data_updated' +DEVICE_INITIALIZED = '{}_device_initialized'.format(DOMAIN) DEFAULT_NAME = 'Yeelight' DEFAULT_TRANSITION = 350 @@ -116,7 +118,7 @@ def setup(hass, config): conf = config.get(DOMAIN, {}) yeelight_data = hass.data[DATA_YEELIGHT] = {} - def device_discovered(service, info): + def device_discovered(_, info): _LOGGER.debug("Adding autodetected %s", info['hostname']) device_type = info['device_type'] @@ -133,7 +135,7 @@ def device_discovered(service, info): discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) - def update(event): + def update(_): for device in list(yeelight_data.values()): device.update() @@ -141,6 +143,17 @@ def update(event): hass, update, conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) ) + def load_platforms(ipaddr): + platform_config = hass.data[DATA_YEELIGHT][ipaddr].config.copy() + platform_config[CONF_HOST] = ipaddr + platform_config[CONF_CUSTOM_EFFECTS] = \ + config.get(DOMAIN, {}).get(CONF_CUSTOM_EFFECTS, {}) + load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, config) + load_platform(hass, BINARY_SENSOR_DOMAIN, DOMAIN, platform_config, + config) + + dispatcher_connect(hass, DEVICE_INITIALIZED, load_platforms) + if DOMAIN in config: for ipaddr, device_config in conf[CONF_DEVICES].items(): _LOGGER.debug("Adding configured %s", device_config[CONF_NAME]) @@ -149,7 +162,7 @@ def update(event): return True -def _setup_device(hass, hass_config, ipaddr, device_config): +def _setup_device(hass, _, ipaddr, device_config): devices = hass.data[DATA_YEELIGHT] if ipaddr in devices: @@ -158,15 +171,7 @@ def _setup_device(hass, hass_config, ipaddr, device_config): device = YeelightDevice(hass, ipaddr, device_config) devices[ipaddr] = device - - platform_config = device_config.copy() - platform_config[CONF_HOST] = ipaddr - platform_config[CONF_CUSTOM_EFFECTS] = \ - hass_config.get(DOMAIN, {}).get(CONF_CUSTOM_EFFECTS, {}) - - load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, hass_config) - load_platform(hass, BINARY_SENSOR_DOMAIN, DOMAIN, platform_config, - hass_config) + hass.add_job(device.setup) class YeelightDevice: @@ -174,31 +179,21 @@ class YeelightDevice: def __init__(self, hass, ipaddr, config): """Initialize device.""" + import yeelight + self._hass = hass self._config = config self._ipaddr = ipaddr self._name = config.get(CONF_NAME) self._model = config.get(CONF_MODEL) - self._bulb_device = None + self._bulb_device = yeelight.Bulb(self.ipaddr, model=self._model) + self._device_type = None self._available = False + self._initialized = False @property def bulb(self): """Return bulb device.""" - if self._bulb_device is None: - import yeelight - try: - self._bulb_device = yeelight.Bulb(self._ipaddr, - model=self._model) - # force init for type - self.update() - - self._available = True - except yeelight.BulbException as ex: - self._available = False - _LOGGER.error("Failed to connect to bulb %s, %s: %s", - self._ipaddr, self._name, ex) - return self._bulb_device @property @@ -221,23 +216,38 @@ def available(self): """Return true is device is available.""" return self._available + @property + def model(self): + """Return configured device model.""" + return self._model + @property def is_nightlight_enabled(self) -> bool: """Return true / false if nightlight is currently enabled.""" if self.bulb is None: return False - return self.bulb.last_properties.get('active_mode') == '1' + return self._active_mode == '1' @property def is_nightlight_supported(self) -> bool: """Return true / false if nightlight is supported.""" - return self.bulb.get_model_specs().get('night_light', False) + if self.model: + return self.bulb.get_model_specs().get('night_light', False) + + return self._active_mode is not None @property - def is_ambilight_supported(self) -> bool: - """Return true / false if ambilight is supported.""" - return self.bulb.get_model_specs().get('background_light', False) + def _active_mode(self): + return self.bulb.last_properties.get('active_mode') + + @property + def type(self): + """Return bulb type.""" + if not self._device_type: + self._device_type = self.bulb.bulb_type + + return self._device_type def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn on device.""" @@ -247,7 +257,6 @@ def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None): self.bulb.turn_on(duration=duration, light_type=light_type) except BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) - return def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn off device.""" @@ -256,10 +265,10 @@ def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): try: self.bulb.turn_off(duration=duration, light_type=light_type) except BulbException as ex: - _LOGGER.error("Unable to turn the bulb off: %s", ex) - return + _LOGGER.error("Unable to turn the bulb off: %s, %s: %s", + self.ipaddr, self.name, ex) - def update(self): + def _update_properties(self): """Read new properties from the device.""" from yeelight import BulbException @@ -271,7 +280,27 @@ def update(self): self._available = True except BulbException as ex: if self._available: # just inform once - _LOGGER.error("Unable to update bulb status: %s", ex) + _LOGGER.error("Unable to update device %s, %s: %s", + self.ipaddr, self.name, ex) self._available = False + return self._available + + def _initialize_if_not_already(self): + if self._initialized: + return + + self._initialized = True + dispatcher_send(self._hass, DEVICE_INITIALIZED, self.ipaddr) + + def update(self): + """Update device properties and send data updated signal.""" + if self._update_properties(): + self._initialize_if_not_already() + dispatcher_send(self._hass, DATA_UPDATED.format(self._ipaddr)) + + def setup(self): + """Initialize device and send initialized signal.""" + if self._update_properties() or self.model: + self._initialize_if_not_already() diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 8aa5c3d7300c2..720e9575a6158 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -28,14 +28,13 @@ SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | - SUPPORT_FLASH) + SUPPORT_FLASH | + SUPPORT_EFFECT) SUPPORT_YEELIGHT_WHITE_TEMP = (SUPPORT_YEELIGHT | SUPPORT_COLOR_TEMP) -SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT | - SUPPORT_COLOR | - SUPPORT_EFFECT | +SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR_TEMP) ATTR_MODE = 'mode' @@ -61,24 +60,33 @@ EFFECT_TWITTER = "Twitter" EFFECT_STOP = "Stop" -YEELIGHT_EFFECT_LIST = [ - EFFECT_DISCO, +YEELIGHT_TEMP_ONLY_EFFECT_LIST = [ EFFECT_TEMP, + EFFECT_STOP, +] + +YEELIGHT_MONO_EFFECT_LIST = [ + EFFECT_DISCO, EFFECT_STROBE, - EFFECT_STROBE_COLOR, EFFECT_ALARM, - EFFECT_POLICE, EFFECT_POLICE2, + EFFECT_WHATSAPP, + EFFECT_FACEBOOK, + EFFECT_TWITTER, + *YEELIGHT_TEMP_ONLY_EFFECT_LIST +] + +YEELIGHT_COLOR_EFFECT_LIST = [ + EFFECT_STROBE_COLOR, + EFFECT_POLICE, EFFECT_CHRISTMAS, EFFECT_RGB, EFFECT_RANDOM_LOOP, EFFECT_FAST_RANDOM_LOOP, EFFECT_LSD, EFFECT_SLOWDOWN, - EFFECT_WHATSAPP, - EFFECT_FACEBOOK, - EFFECT_TWITTER, - EFFECT_STOP] + *YEELIGHT_MONO_EFFECT_LIST +] def _transitions_config_parser(transitions): @@ -127,7 +135,7 @@ def _wrap(self, *args, **kwargs): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight bulbs.""" - from yeelight.enums import PowerMode + from yeelight.enums import PowerMode, BulbType data_key = '{}_lights'.format(DATA_YEELIGHT) @@ -140,13 +148,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device = hass.data[DATA_YEELIGHT][discovery_info[CONF_HOST]] _LOGGER.debug("Adding %s", device.name) + model = device.model custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS]) - lights = [YeelightLight(device, custom_effects=custom_effects)] - - if device.is_ambilight_supported: - lights.append( - YeelightAmbientLight(device, custom_effects=custom_effects)) + lights = [] + + def _lights_setup_helper(klass): + lights.append(klass(device, custom_effects=custom_effects)) + + if model in ('mono', 'mono1') or device.type == BulbType.White: + _lights_setup_helper(YeelightGenericLight) + elif model in ('color', 'color1', 'color2', 'strip1', 'bslamp1') or \ + device.type == BulbType.Color: + _lights_setup_helper(YeelightColorLight) + elif model in ('ceiling1', 'ceiling2', 'ceiling3') or \ + device.type == BulbType.WhiteTemp: + _lights_setup_helper(YeelightWhiteTempLight) + elif model == 'ceiling4' or device.type == BulbType.WhiteTempMood: + _lights_setup_helper(YeelightWithAmbientLight) + _lights_setup_helper(YeelightAmbientLight) + else: + _LOGGER.error("Cannot determinate device type for %s, %s", + device.ipaddr, device.name) hass.data[data_key] += lights add_entities(lights, True) @@ -184,8 +207,8 @@ def service_handler(service): schema=service_schema_start_flow) -class YeelightLight(Light): - """Representation of a Yeelight light.""" +class YeelightGenericLight(Light): + """Representation of a Yeelight generic light.""" def __init__(self, device, custom_effects=None): """Initialize the Yeelight light.""" @@ -194,15 +217,13 @@ def __init__(self, device, custom_effects=None): self.config = device.config self._device = device - self._supported_features = SUPPORT_YEELIGHT - self._brightness = None self._color_temp = None - self._is_on = None self._hs = None - self._min_mireds = None - self._max_mireds = None + model_specs = self._bulb.get_model_specs() + self._min_mireds = kelvin_to_mired(model_specs['color_temp']['max']) + self._max_mireds = kelvin_to_mired(model_specs['color_temp']['min']) self._light_type = LightType.Main @@ -236,17 +257,20 @@ def available(self) -> bool: @property def supported_features(self) -> int: """Flag supported features.""" - return self._supported_features + return SUPPORT_YEELIGHT @property def effect_list(self): """Return the list of supported effects.""" - return YEELIGHT_EFFECT_LIST + self.custom_effects_names + return self._predefined_effects + self.custom_effects_names @property def color_temp(self) -> int: """Return the color temperature.""" - return self._color_temp + temp = self._get_property('ct') + if temp: + self._color_temp = temp + return kelvin_to_mired(int(self._color_temp)) @property def name(self) -> str: @@ -256,12 +280,15 @@ def name(self) -> str: @property def is_on(self) -> bool: """Return true if device is on.""" - return self._is_on + return self._get_property('power') == 'on' @property def brightness(self) -> int: """Return the brightness of this light between 1..255.""" - return self._brightness + temp = self._get_property(self._brightness_property) + if temp: + self._brightness = temp + return round(255 * (int(self._brightness) / 100)) @property def min_mireds(self): @@ -288,6 +315,42 @@ def light_type(self): """Return light type.""" return self._light_type + @property + def hs_color(self) -> tuple: + """Return the color property.""" + return self._hs + + # F821: https://github.com/PyCQA/pyflakes/issues/373 + @property + def _bulb(self) -> 'yeelight.Bulb': # noqa: F821 + return self.device.bulb + + @property + def _properties(self) -> dict: + if self._bulb is None: + return {} + return self._bulb.last_properties + + def _get_property(self, prop, default=None): + return self._properties.get(prop, default) + + @property + def _brightness_property(self): + return 'bright' + + @property + def _predefined_effects(self): + return YEELIGHT_MONO_EFFECT_LIST + + @property + def device(self): + """Return yeelight device.""" + return self._device + + def update(self): + """Update light properties.""" + self._hs = self._get_hs_from_properties() + def _get_hs_from_properties(self): rgb = self._get_property('rgb') color_mode = self._get_property('color_mode') @@ -297,7 +360,7 @@ def _get_hs_from_properties(self): color_mode = int(color_mode) if color_mode == 2: # color temperature - temp_in_k = mired_to_kelvin(self._color_temp) + temp_in_k = mired_to_kelvin(self.color_temp) return color_util.color_temperature_to_hs(temp_in_k) if color_mode == 3: # hsv hue = int(self._get_property('hue')) @@ -312,34 +375,6 @@ def _get_hs_from_properties(self): return color_util.color_RGB_to_hs(red, green, blue) - @property - def hs_color(self) -> tuple: - """Return the color property.""" - return self._hs - - @property - def _properties(self) -> dict: - if self._bulb is None: - return {} - return self._bulb.last_properties - - def _get_property(self, prop, default=None): - return self._properties.get(prop, default) - - @property - def device(self): - """Return yeelight device.""" - return self._device - - @property - def _is_nightlight_enabled(self): - return self.device.is_nightlight_enabled - - # F821: https://github.com/PyCQA/pyflakes/issues/373 - @property - def _bulb(self) -> 'yeelight.Bulb': # noqa: F821 - return self.device.bulb - def set_music_mode(self, mode) -> None: """Set the music mode on or off.""" if mode: @@ -347,49 +382,6 @@ def set_music_mode(self, mode) -> None: else: self._bulb.stop_music() - def update(self) -> None: - """Update properties from the bulb.""" - import yeelight - bulb_type = self._bulb.bulb_type - - if bulb_type == yeelight.BulbType.Color: - self._supported_features = SUPPORT_YEELIGHT_RGB - elif self.light_type == yeelight.enums.LightType.Ambient: - self._supported_features = SUPPORT_YEELIGHT_RGB - elif bulb_type in (yeelight.BulbType.WhiteTemp, - yeelight.BulbType.WhiteTempMood): - if self._is_nightlight_enabled: - self._supported_features = SUPPORT_YEELIGHT - else: - self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP - - if self.min_mireds is None: - model_specs = self._bulb.get_model_specs() - self._min_mireds = \ - kelvin_to_mired(model_specs['color_temp']['max']) - self._max_mireds = \ - kelvin_to_mired(model_specs['color_temp']['min']) - - if bulb_type == yeelight.BulbType.WhiteTempMood: - self._is_on = self._get_property('main_power') == 'on' - else: - self._is_on = self._get_property('power') == 'on' - - if self._is_nightlight_enabled: - bright = self._get_property('nl_br') - else: - bright = self._get_property('bright') - - if bright: - self._brightness = round(255 * (int(bright) / 100)) - - temp_in_k = self._get_property('ct') - - if temp_in_k: - self._color_temp = kelvin_to_mired(int(temp_in_k)) - - self._hs = self._get_hs_from_properties() - @_cmd def set_brightness(self, brightness, duration) -> None: """Set bulb brightness.""" @@ -583,12 +575,50 @@ def start_flow(self, transitions, count=0, action=ACTION_RECOVER): _LOGGER.error("Unable to set effect: %s", ex) -class YeelightAmbientLight(YeelightLight): +class YeelightColorLight(YeelightGenericLight): + """Representation of a Color Yeelight light.""" + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_YEELIGHT_RGB + + @property + def _predefined_effects(self): + return YEELIGHT_COLOR_EFFECT_LIST + + +class YeelightWhiteTempLight(YeelightGenericLight): + """Representation of a Color Yeelight light.""" + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_YEELIGHT_WHITE_TEMP + + @property + def _brightness_property(self): + return 'current_brightness' + + @property + def _predefined_effects(self): + return YEELIGHT_TEMP_ONLY_EFFECT_LIST + + +class YeelightWithAmbientLight(YeelightWhiteTempLight): + """Representation of a Yeelight which has ambilight support.""" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._get_property('main_power') == 'on' + + +class YeelightAmbientLight(YeelightColorLight): """Representation of a Yeelight ambient light.""" PROPERTIES_MAPPING = { "color_mode": "bg_lmode", - "main_power": "bg_power", } def __init__(self, *args, **kwargs): @@ -606,14 +636,10 @@ def name(self) -> str: """Return the name of the device if any.""" return "{} ambilight".format(self.device.name) - @property - def _is_nightlight_enabled(self): - return False - def _get_property(self, prop, default=None): bg_prop = self.PROPERTIES_MAPPING.get(prop) if not bg_prop: bg_prop = "bg_" + prop - return self._properties.get(bg_prop, default) + return super()._get_property(bg_prop, default)