From 842a36dc9ee5f4a1d27fdc8bb62cdeb8a2d8a9ae Mon Sep 17 00:00:00 2001 From: OleksandrBerchenko Date: Sun, 31 Mar 2019 22:02:45 +0300 Subject: [PATCH] Rewrite Osram Lightify component (#22184) * Rewrite Osram Lightify component * Update python-lightify version to 1.0.7.2 * Remove unneeded code * 1. Remove changes in light/__init__.py, 2. Set properties to None by default * Fix typo --- .../components/osramlightify/light.py | 414 +++++++++++------- requirements_all.txt | 2 +- 2 files changed, 246 insertions(+), 170 deletions(-) diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index a49e12c76a6..59cc2bac5d6 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -4,41 +4,35 @@ Support for Osram Lightify. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.osramlightify/ """ -from datetime import timedelta import logging import random import socket import voluptuous as vol -from homeassistant import util from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - ATTR_TRANSITION, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_EFFECT, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, Light) + from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin) import homeassistant.util.color as color_util -REQUIREMENTS = ['lightify==1.0.6.1'] +REQUIREMENTS = ['lightify==1.0.7.2'] _LOGGER = logging.getLogger(__name__) CONF_ALLOW_LIGHTIFY_NODES = 'allow_lightify_nodes' CONF_ALLOW_LIGHTIFY_GROUPS = 'allow_lightify_groups' +CONF_INTERVAL_LIGHTIFY_STATUS = 'interval_lightify_status' +CONF_INTERVAL_LIGHTIFY_CONF = 'interval_lightify_conf' DEFAULT_ALLOW_LIGHTIFY_NODES = True DEFAULT_ALLOW_LIGHTIFY_GROUPS = True - -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - -SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | - SUPPORT_EFFECT | SUPPORT_COLOR | - SUPPORT_TRANSITION) +DEFAULT_INTERVAL_LIGHTIFY_STATUS = 5 +DEFAULT_INTERVAL_LIGHTIFY_CONF = 3600 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -46,228 +40,310 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ default=DEFAULT_ALLOW_LIGHTIFY_NODES): cv.boolean, vol.Optional(CONF_ALLOW_LIGHTIFY_GROUPS, default=DEFAULT_ALLOW_LIGHTIFY_GROUPS): cv.boolean, + vol.Optional(CONF_INTERVAL_LIGHTIFY_STATUS, + default=DEFAULT_INTERVAL_LIGHTIFY_STATUS): cv.positive_int, + vol.Optional(CONF_INTERVAL_LIGHTIFY_CONF, + default=DEFAULT_INTERVAL_LIGHTIFY_CONF): cv.positive_int }) +DEFAULT_BRIGHTNESS = 2 +DEFAULT_KELVIN = 2700 -def setup_platform(hass, config, add_entities, discovery_info=None): + +def setup_platform(_hass, config, add_entities, _discovery_info=None): """Set up the Osram Lightify lights.""" import lightify - host = config.get(CONF_HOST) - add_nodes = config.get(CONF_ALLOW_LIGHTIFY_NODES) - add_groups = config.get(CONF_ALLOW_LIGHTIFY_GROUPS) - + host = config[CONF_HOST] try: - bridge = lightify.Lightify(host) + bridge = lightify.Lightify(host, log_level=logging.NOTSET) except socket.error as err: msg = "Error connecting to bridge: {} due to: {}".format( host, str(err)) _LOGGER.exception(msg) return - setup_bridge(bridge, add_entities, add_nodes, add_groups) + setup_bridge(bridge, add_entities, config) -def setup_bridge(bridge, add_entities, add_nodes, add_groups): +def setup_bridge(bridge, add_entities, config): """Set up the Lightify bridge.""" lights = {} + groups = {} + groups_last_updated = [0] - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_lights(): - """Update the lights objects with latest info from bridge.""" + """Update the lights objects with the latest info from the bridge.""" try: - bridge.update_all_light_status() - bridge.update_group_list() + new_lights = bridge.update_all_light_status( + config[CONF_INTERVAL_LIGHTIFY_STATUS]) + lights_changed = bridge.lights_changed() except TimeoutError: _LOGGER.error("Timeout during updating of lights") + return 0 except OSError: _LOGGER.error("OSError during updating of lights") + return 0 - new_lights = [] - - if add_nodes: - for (light_id, light) in bridge.lights().items(): - if light_id not in lights: - osram_light = OsramLightifyLight( - light_id, light, update_lights) - lights[light_id] = osram_light - new_lights.append(osram_light) + if new_lights and config[CONF_ALLOW_LIGHTIFY_NODES]: + new_entities = [] + for addr, light in new_lights.items(): + if addr not in lights: + osram_light = OsramLightifyLight(light, update_lights, + lights_changed) + lights[addr] = osram_light + new_entities.append(osram_light) else: - lights[light_id].light = light + lights[addr].update_luminary(light) - if add_groups: - for (group_name, group) in bridge.groups().items(): - if group_name not in lights: - osram_group = OsramLightifyGroup( - group, bridge, update_lights) - lights[group_name] = osram_group - new_lights.append(osram_group) + add_entities(new_entities) + + return lights_changed + + def update_groups(): + """Update the groups objects with the latest info from the bridge.""" + lights_changed = update_lights() + + try: + new_groups = bridge.update_group_list( + config[CONF_INTERVAL_LIGHTIFY_CONF]) + groups_updated = bridge.groups_updated() + except TimeoutError: + _LOGGER.error("Timeout during updating of groups") + return 0 + except OSError: + _LOGGER.error("OSError during updating of groups") + return 0 + + if new_groups: + new_groups = {group.idx(): group for group in new_groups.values()} + new_entities = [] + for idx, group in new_groups.items(): + if idx not in groups: + osram_group = OsramLightifyGroup(group, update_groups, + groups_updated) + groups[idx] = osram_group + new_entities.append(osram_group) else: - lights[group_name].group = group + groups[idx].update_luminary(group) - if new_lights: - add_entities(new_lights) + add_entities(new_entities) + + if groups_updated > groups_last_updated[0]: + groups_last_updated[0] = groups_updated + for idx, osram_group in groups.items(): + if idx not in new_groups: + osram_group.update_static_attributes() + + return max(lights_changed, groups_updated) update_lights() + if config[CONF_ALLOW_LIGHTIFY_GROUPS]: + update_groups() class Luminary(Light): """Representation of Luminary Lights and Groups.""" - def __init__(self, luminary, update_lights): - """Initialize a Luminary light.""" - self.update_lights = update_lights + def __init__(self, luminary, update_func, changed): + """Initialize a Luminary Light.""" + self.update_func = update_func self._luminary = luminary + self._changed = changed + + self._unique_id = None + self._supported_features = [] + self._effect_list = [] + self._is_on = False + self._min_mireds = None + self._max_mireds = None self._brightness = None - self._hs = None - self._name = None - self._temperature = None - self._state = False - self.update() + self._color_temp = None + self._rgb_color = None + + self.update_static_attributes() + self.update_dynamic_attributes() + + def _get_unique_id(self): + """Get a unique ID (not implemented).""" + raise NotImplementedError + + def _get_supported_features(self): + """Get list of supported features.""" + features = 0 + if 'lum' in self._luminary.supported_features(): + features = features | SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + + if 'temp' in self._luminary.supported_features(): + features = features | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION + + if 'rgb' in self._luminary.supported_features(): + features = (features | SUPPORT_COLOR | SUPPORT_TRANSITION | + SUPPORT_EFFECT) + + return features + + def _get_effect_list(self): + """Get list of supported effects.""" + effects = [] + if 'rgb' in self._luminary.supported_features(): + effects.append(EFFECT_RANDOM) + + return effects @property def name(self): - """Return the name of the device if any.""" - return self._name + """Return the name of the luminary.""" + return self._luminary.name() @property def hs_color(self): - """Last hs color value set.""" - return self._hs + """Return last hs color value set.""" + return color_util.color_RGB_to_hs(*self._rgb_color) @property def color_temp(self): """Return the color temperature.""" - return self._temperature + return self._color_temp @property def brightness(self): - """Brightness of this light between 0..255.""" + """Return brightness of the luminary (0..255).""" return self._brightness @property def is_on(self): - """Update status to True if device is on.""" - return self._state + """Return True if the device is on.""" + return self._is_on @property def supported_features(self): - """Flag supported features.""" - return SUPPORT_OSRAMLIGHTIFY + """List of supported features.""" + return self._supported_features @property def effect_list(self): """List of supported effects.""" - return [EFFECT_RANDOM] - - def turn_on(self, **kwargs): - """Turn the device on.""" - if ATTR_TRANSITION in kwargs: - transition = int(kwargs[ATTR_TRANSITION] * 10) - else: - transition = 0 - - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - self._luminary.set_luminance( - int(self._brightness / 2.55), transition) - else: - self._luminary.set_onoff(1) - - if ATTR_HS_COLOR in kwargs: - red, green, blue = \ - color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) - self._luminary.set_rgb(red, green, blue, transition) - - if ATTR_COLOR_TEMP in kwargs: - color_t = kwargs[ATTR_COLOR_TEMP] - kelvin = int(color_temperature_mired_to_kelvin(color_t)) - self._luminary.set_temperature(kelvin, transition) - - if ATTR_EFFECT in kwargs: - effect = kwargs.get(ATTR_EFFECT) - if effect == EFFECT_RANDOM: - self._luminary.set_rgb( - random.randrange(0, 255), random.randrange(0, 255), - random.randrange(0, 255), transition) - - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - if ATTR_TRANSITION in kwargs: - transition = int(kwargs[ATTR_TRANSITION] * 10) - self._luminary.set_luminance(0, transition) - else: - transition = 0 - self._luminary.set_onoff(0) - self.schedule_update_ha_state() - - def update(self): - """Synchronize state with bridge.""" - self.update_lights(no_throttle=True) - self._name = self._luminary.name() - - -class OsramLightifyLight(Luminary): - """Representation of an Osram Lightify Light.""" - - def __init__(self, light_id, light, update_lights): - """Initialize the Lightify light.""" - self._light_id = light_id - super().__init__(light, update_lights) - - def update(self): - """Update status of a light.""" - super().update() - self._state = self._luminary.on() - rgb = self._luminary.rgb() - self._hs = color_util.color_RGB_to_hs(*rgb) - o_temp = self._luminary.temp() - if o_temp == 0: - self._temperature = None - else: - self._temperature = color_temperature_kelvin_to_mired( - self._luminary.temp()) - self._brightness = int(self._luminary.lum() * 2.55) + return self._effect_list @property - def unique_id(self): - """Return a unique ID.""" - return self._light_id + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return self._min_mireds - -class OsramLightifyGroup(Luminary): - """Representation of an Osram Lightify Group.""" - - def __init__(self, group, bridge, update_lights): - """Initialize the Lightify light group.""" - self._bridge = bridge - self._light_ids = [] - super().__init__(group, update_lights) - self._unique_id = '{}'.format(self._light_ids) - - def _get_state(self): - """Get state of group.""" - lights = self._bridge.lights() - return any(lights[light_id].on() for light_id in self._light_ids) - - def update(self): - """Update group status.""" - super().update() - self._light_ids = self._luminary.lights() - light = self._bridge.lights()[self._light_ids[0]] - self._brightness = int(light.lum() * 2.55) - rgb = light.rgb() - self._hs = color_util.color_RGB_to_hs(*rgb) - o_temp = light.temp() - if o_temp == 0: - self._temperature = None - else: - self._temperature = color_temperature_kelvin_to_mired(o_temp) - self._state = light.on() + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return self._max_mireds @property def unique_id(self): """Return a unique ID.""" return self._unique_id + + def play_effect(self, effect, transition): + """Play selected effect.""" + if effect == EFFECT_RANDOM: + self._rgb_color = (random.randrange(0, 256), + random.randrange(0, 256), + random.randrange(0, 256)) + self._luminary.set_rgb(*self._rgb_color, transition) + self._luminary.set_onoff(True) + return True + + return False + + def turn_on(self, **kwargs): + """Turn the device on.""" + transition = int(kwargs.get(ATTR_TRANSITION, 0) * 10) + if ATTR_EFFECT in kwargs: + self.play_effect(kwargs[ATTR_EFFECT], transition) + return + + if ATTR_HS_COLOR in kwargs: + self._rgb_color = color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR]) + self._luminary.set_rgb(*self._rgb_color, transition) + + if ATTR_COLOR_TEMP in kwargs: + self._color_temp = kwargs[ATTR_COLOR_TEMP] + self._luminary.set_temperature( + int(color_util.color_temperature_mired_to_kelvin( + self._color_temp)), transition) + + self._is_on = True + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + self._luminary.set_luminance(int(self._brightness / 2.55), + transition) + else: + self._luminary.set_onoff(True) + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._is_on = False + if ATTR_TRANSITION in kwargs: + transition = int(kwargs[ATTR_TRANSITION] * 10) + self._brightness = DEFAULT_BRIGHTNESS + self._luminary.set_luminance(0, transition) + else: + self._luminary.set_onoff(False) + + def update_luminary(self, luminary): + """Update internal luminary object.""" + self._luminary = luminary + self.update_static_attributes() + + def update_static_attributes(self): + """Update static attributes of the luminary.""" + self._unique_id = self._get_unique_id() + self._supported_features = self._get_supported_features() + self._effect_list = self._get_effect_list() + if self._supported_features & SUPPORT_COLOR_TEMP: + self._min_mireds = color_util.color_temperature_kelvin_to_mired( + self._luminary.max_temp() or DEFAULT_KELVIN) + self._max_mireds = color_util.color_temperature_kelvin_to_mired( + self._luminary.min_temp() or DEFAULT_KELVIN) + + def update_dynamic_attributes(self): + """Update dynamic attributes of the luminary.""" + self._is_on = self._luminary.on() + if self._supported_features & SUPPORT_BRIGHTNESS: + self._brightness = int(self._luminary.lum() * 2.55) + + if self._supported_features & SUPPORT_COLOR_TEMP: + self._color_temp = color_util.color_temperature_kelvin_to_mired( + self._luminary.temp() or DEFAULT_KELVIN) + + if self._supported_features & SUPPORT_COLOR: + self._rgb_color = self._luminary.rgb() + + def update(self): + """Synchronize state with bridge.""" + changed = self.update_func() + if changed > self._changed: + self._changed = changed + self.update_dynamic_attributes() + + +class OsramLightifyLight(Luminary): + """Representation of an Osram Lightify Light.""" + + def _get_unique_id(self): + """Get a unique ID.""" + return self._luminary.addr() + + +class OsramLightifyGroup(Luminary): + """Representation of an Osram Lightify Group.""" + + def _get_unique_id(self): + """Get a unique ID for the group.""" +# Actually, it's a wrong choice for a unique ID, because a combination of +# lights is NOT unique (Osram Lightify allows to create different groups +# with the same lights). Also a combination of lights may easily change, +# but the group remains the same from the user's perspective. +# It should be something like "-" +# For now keeping it as is for backward compatibility with existing +# users. + return '{}'.format(self._luminary.lights()) diff --git a/requirements_all.txt b/requirements_all.txt index ec4c6028356..18f9098a2e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -643,7 +643,7 @@ libsoundtouch==0.7.2 liffylights==0.9.4 # homeassistant.components.osramlightify.light -lightify==1.0.6.1 +lightify==1.0.7.2 # homeassistant.components.lightwave lightwave==0.15