From 6d3c3ce449e3aa97604ffce6557dfb450161c418 Mon Sep 17 00:00:00 2001 From: zewelor Date: Thu, 13 Jun 2019 18:42:47 +0200 Subject: [PATCH] Refactor yeelight code (#22547) * Separate yeelight light classes * Removed not used variable * Allow to create device right away, when model is declared * Lint fixes * Use correct brightness, when nightlight mode is on * Pylint fix * Add power property * Fix imports * Update homeassistant/components/yeelight/light.py Co-Authored-By: Teemu R. * Small PR fixes * Simplify device to yeelight class mapping * Simplify device initialization code * Fix comment --- homeassistant/components/yeelight/__init__.py | 103 ++++++--- homeassistant/components/yeelight/light.py | 210 ++++++++++-------- 2 files changed, 185 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index dabd66751fd..39dc62eddb0 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -14,7 +14,8 @@ from homeassistant.components.binary_sensor import DOMAIN as \ 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 _LOGGER = logging.getLogger(__name__) @@ -22,6 +23,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = 'yeelight_{}_data_updated' +DEVICE_INITIALIZED = '{}_device_initialized'.format(DOMAIN) DEFAULT_NAME = 'Yeelight' DEFAULT_TRANSITION = 350 @@ -41,6 +43,8 @@ ACTION_RECOVER = 'recover' ACTION_STAY = 'stay' ACTION_OFF = 'off' +ACTIVE_MODE_NIGHTLIGHT = '1' + SCAN_INTERVAL = timedelta(seconds=30) YEELIGHT_RGB_TRANSITION = 'RGBTransition' @@ -115,7 +119,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'] @@ -132,7 +136,7 @@ def setup(hass, config): discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) - def update(event): + def update(_): for device in list(yeelight_data.values()): device.update() @@ -140,6 +144,17 @@ def setup(hass, config): 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]) @@ -148,7 +163,7 @@ def setup(hass, config): 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: @@ -157,15 +172,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: @@ -178,24 +185,14 @@ class YeelightDevice: self._ipaddr = ipaddr self._name = config.get(CONF_NAME) self._model = config.get(CONF_MODEL) - self._bulb_device = None + self._bulb_device = 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: - try: - self._bulb_device = Bulb(self._ipaddr, model=self._model) - # force init for type - self.update() - - self._available = True - except 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 @@ -218,23 +215,38 @@ class YeelightDevice: """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 == ACTIVE_MODE_NIGHTLIGHT @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.""" @@ -242,17 +254,16 @@ class YeelightDevice: 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.""" 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.""" if not self.bulb: return @@ -260,9 +271,29 @@ class YeelightDevice: try: self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True + if not self._initialized: + self._initialize_device() 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_device(self): + self._initialized = True + dispatcher_send(self._hass, DEVICE_INITIALIZED, self.ipaddr) + + def update(self): + """Update device properties and send data updated signal.""" + self._update_properties() dispatcher_send(self._hass, DATA_UPDATED.format(self._ipaddr)) + + def setup(self): + """Fetch initial device properties.""" + initial_update = self._update_properties() + + # We can build correct class anyway. + if not initial_update and self.model: + self._initialize_device() diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 33116d973e9..563c9ab8782 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -80,6 +80,19 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_TWITTER, EFFECT_STOP] +MODEL_TO_DEVICE_TYPE = { + 'mono': BulbType.White, + 'mono1': BulbType.White, + 'color': BulbType.Color, + 'color1': BulbType.Color, + 'color2': BulbType.Color, + 'strip1': BulbType.Color, + 'bslamp1': BulbType.Color, + 'ceiling1': BulbType.WhiteTemp, + 'ceiling2': BulbType.WhiteTemp, + 'ceiling3': BulbType.WhiteTemp, + 'ceiling4': BulbType.WhiteTempMood} + def _transitions_config_parser(transitions): """Parse transitions config into initialized objects.""" @@ -137,11 +150,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS]) - lights = [YeelightLight(device, custom_effects=custom_effects)] + lights = [] - if device.is_ambilight_supported: - lights.append( - YeelightAmbientLight(device, custom_effects=custom_effects)) + if device.model: + device_type = MODEL_TO_DEVICE_TYPE.get(device.model, None) + else: + device_type = device.type + + def _lights_setup_helper(klass): + lights.append(klass(device, custom_effects=custom_effects)) + + if device_type == BulbType.White: + _lights_setup_helper(YeelightGenericLight) + elif device_type == BulbType.Color: + _lights_setup_helper(YeelightColorLight) + elif device_type == BulbType.WhiteTemp: + _lights_setup_helper(YeelightWhiteTempLight) + elif device_type == BulbType.WhiteTempMood: + _lights_setup_helper(YeelightWithAmbientLight) + _lights_setup_helper(YeelightAmbientLight) + else: + _LOGGER.error("Cannot determine device type for %s, %s", + device.ipaddr, device.name) hass.data[data_key] += lights add_entities(lights, True) @@ -179,23 +209,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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.""" 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 @@ -229,7 +257,7 @@ class YeelightLight(Light): @property def supported_features(self) -> int: """Flag supported features.""" - return self._supported_features + return SUPPORT_YEELIGHT @property def effect_list(self): @@ -239,7 +267,10 @@ class YeelightLight(Light): @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: @@ -249,12 +280,15 @@ class YeelightLight(Light): @property def is_on(self) -> bool: """Return true if device is on.""" - return self._is_on + return self._get_property(self._power_property) == '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): @@ -281,6 +315,42 @@ class YeelightLight(Light): """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) -> '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 _power_property(self): + return 'power' + + @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') @@ -290,7 +360,7 @@ class YeelightLight(Light): 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')) @@ -305,34 +375,6 @@ class YeelightLight(Light): 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: @@ -340,47 +382,6 @@ class YeelightLight(Light): else: self._bulb.stop_music() - def update(self) -> None: - """Update properties from the bulb.""" - bulb_type = self._bulb.bulb_type - - if bulb_type == BulbType.Color: - self._supported_features = SUPPORT_YEELIGHT_RGB - elif self.light_type == LightType.Ambient: - self._supported_features = SUPPORT_YEELIGHT_RGB - elif bulb_type in (BulbType.WhiteTemp, 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 == 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.""" @@ -566,12 +567,41 @@ class YeelightLight(Light): _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 + + +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' + + +class YeelightWithAmbientLight(YeelightWhiteTempLight): + """Representation of a Yeelight which has ambilight support.""" + + @ property + def _power_property(self): + return 'main_power' + + +class YeelightAmbientLight(YeelightColorLight): """Representation of a Yeelight ambient light.""" PROPERTIES_MAPPING = { "color_mode": "bg_lmode", - "main_power": "bg_power", } def __init__(self, *args, **kwargs): @@ -587,14 +617,10 @@ class YeelightAmbientLight(YeelightLight): """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)