From 9214934d47871575d6b235dd547fc1ccab79c029 Mon Sep 17 00:00:00 2001 From: zewelor <zewelor@gmail.com> Date: Sun, 24 Mar 2019 13:01:12 +0100 Subject: [PATCH] Move yeelight into component (#21593) --- .../components/discovery/__init__.py | 3 +- homeassistant/components/light/services.yaml | 26 -- homeassistant/components/yeelight/__init__.py | 358 +++++++++++++++++- homeassistant/components/yeelight/light.py | 306 ++++----------- .../components/yeelight/services.yaml | 25 ++ requirements_all.txt | 2 +- 6 files changed, 450 insertions(+), 270 deletions(-) create mode 100644 homeassistant/components/yeelight/services.yaml diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index d4816213f50..1fb727642bc 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -46,6 +46,7 @@ SERVICE_ROKU = 'roku' SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_TELLDUSLIVE = 'tellstick' +SERVICE_YEELIGHT = 'yeelight' SERVICE_WEMO = 'belkin_wemo' SERVICE_WINK = 'wink' SERVICE_XIAOMI_GW = 'xiaomi_gw' @@ -79,6 +80,7 @@ SERVICE_HANDLERS = { SERVICE_KONNECTED: ('konnected', None), SERVICE_OCTOPRINT: ('octoprint', None), SERVICE_FREEBOX: ('freebox', None), + SERVICE_YEELIGHT: ('yeelight', None), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), 'yamaha': ('media_player', 'yamaha'), @@ -86,7 +88,6 @@ SERVICE_HANDLERS = { 'directv': ('media_player', 'directv'), 'denonavr': ('media_player', 'denonavr'), 'samsung_tv': ('media_player', 'samsungtv'), - 'yeelight': ('light', 'yeelight'), 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), 'harmony': ('remote', 'harmony'), diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index a2863482477..cdf82e97429 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -178,29 +178,3 @@ xiaomi_miio_set_delayed_turn_off: time_period: description: Time period for the delayed turn off. example: "5, '0:05', {'minutes': 5}" - -yeelight_set_mode: - description: Set a operation mode. - fields: - entity_id: - description: Name of the light entity. - example: 'light.yeelight' - mode: - description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. - example: 'moonlight' - -yeelight_start_flow: - description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects - fields: - entity_id: - description: Name of the light entity. - example: 'light.yeelight' - count: - description: The number of times to run this flow (0 to run forever). - example: 0 - action: - description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') - example: 'stay' - transitions: - description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html - example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index d8c1f23bcbb..32e3c5f69e3 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1 +1,357 @@ -"""The yeelight component.""" +""" +Support for Xiaomi Yeelight Wifi color bulb. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/yeelight/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol +from homeassistant.components.discovery import SERVICE_YEELIGHT +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_SCAN_INTERVAL, \ + CONF_HOST, ATTR_ENTITY_ID, CONF_LIGHTS +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +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.event import async_track_time_interval +from homeassistant.helpers.service import extract_entity_ids + +REQUIREMENTS = ['yeelight==0.4.3'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "yeelight" +DATA_YEELIGHT = DOMAIN +DATA_UPDATED = '{}_data_updated'.format(DOMAIN) + +DEFAULT_NAME = 'Yeelight' +DEFAULT_TRANSITION = 350 + +CONF_MODEL = 'model' +CONF_TRANSITION = 'transition' +CONF_SAVE_ON_CHANGE = 'save_on_change' +CONF_MODE_MUSIC = 'use_music_mode' +CONF_FLOW_PARAMS = 'flow_params' +CONF_CUSTOM_EFFECTS = 'custom_effects' + +ATTR_MODE = 'mode' +ATTR_COUNT = 'count' +ATTR_ACTION = 'action' +ATTR_TRANSITIONS = 'transitions' + +ACTION_RECOVER = 'recover' +ACTION_STAY = 'stay' +ACTION_OFF = 'off' + +MODE_MOONLIGHT = 'moonlight' +MODE_DAYLIGHT = 'normal' + +SCAN_INTERVAL = timedelta(seconds=30) + +YEELIGHT_RGB_TRANSITION = 'RGBTransition' +YEELIGHT_HSV_TRANSACTION = 'HSVTransition' +YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition' +YEELIGHT_SLEEP_TRANSACTION = 'SleepTransition' + +SERVICE_SET_MODE = 'set_mode' +SERVICE_START_FLOW = 'start_flow' + +YEELIGHT_FLOW_TRANSITION_SCHEMA = { + vol.Optional(ATTR_COUNT, default=0): cv.positive_int, + vol.Optional(ATTR_ACTION, default=ACTION_RECOVER): + vol.Any(ACTION_RECOVER, ACTION_OFF, ACTION_STAY), + vol.Required(ATTR_TRANSITIONS): [{ + vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION): + vol.All(cv.ensure_list, [cv.positive_int]), + vol.Exclusive(YEELIGHT_HSV_TRANSACTION, CONF_TRANSITION): + vol.All(cv.ensure_list, [cv.positive_int]), + vol.Exclusive(YEELIGHT_TEMPERATURE_TRANSACTION, CONF_TRANSITION): + vol.All(cv.ensure_list, [cv.positive_int]), + vol.Exclusive(YEELIGHT_SLEEP_TRANSACTION, CONF_TRANSITION): + vol.All(cv.ensure_list, [cv.positive_int]), + }] +} + +DEVICE_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, + vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, + vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean, + vol.Optional(CONF_MODEL): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_CUSTOM_EFFECTS): [{ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA + }] + }), +}, extra=vol.ALLOW_EXTRA) + +YEELIGHT_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + +NIGHTLIGHT_SUPPORTED_MODELS = [ + "ceiling3", + 'ceiling4' +] + +UPDATE_REQUEST_PROPERTIES = [ + "power", + "bright", + "ct", + "rgb", + "hue", + "sat", + "color_mode", + "flowing", + "music_on", + "nl_br", + "active_mode", +] + + +def _transitions_config_parser(transitions): + """Parse transitions config into initialized objects.""" + import yeelight + + transition_objects = [] + for transition_config in transitions: + transition, params = list(transition_config.items())[0] + transition_objects.append(getattr(yeelight, transition)(*params)) + + return transition_objects + + +def _parse_custom_effects(effects_config): + import yeelight + + effects = {} + for config in effects_config: + params = config[CONF_FLOW_PARAMS] + action = yeelight.Flow.actions[params[ATTR_ACTION]] + transitions = _transitions_config_parser( + params[ATTR_TRANSITIONS]) + + effects[config[CONF_NAME]] = { + ATTR_COUNT: params[ATTR_COUNT], + ATTR_ACTION: action, + ATTR_TRANSITIONS: transitions + } + + return effects + + +def setup(hass, config): + """Set up the Yeelight bulbs.""" + from yeelight.enums import PowerMode + + conf = config[DOMAIN] + yeelight_data = hass.data[DATA_YEELIGHT] = { + CONF_DEVICES: {}, + CONF_LIGHTS: {}, + } + + def device_discovered(service, info): + _LOGGER.debug("Adding autodetected %s", info['hostname']) + + device_type = info['device_type'] + + name = "yeelight_%s_%s" % (device_type, + info['properties']['mac']) + ipaddr = info[CONF_HOST] + device_config = DEVICE_SCHEMA({ + CONF_NAME: name, + CONF_MODEL: device_type + }) + + _setup_device(hass, config, ipaddr, device_config) + + discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) + + def async_update(event): + for device in yeelight_data[CONF_DEVICES].values(): + device.update() + + async_track_time_interval( + hass, async_update, conf[CONF_SCAN_INTERVAL] + ) + + def service_handler(service): + """Dispatch service calls to target entities.""" + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + + entity_ids = extract_entity_ids(hass, service) + target_devices = [dev.device for dev in + yeelight_data[CONF_LIGHTS].values() + if dev.entity_id in entity_ids] + + for target_device in target_devices: + if service.service == SERVICE_SET_MODE: + target_device.set_mode(**params) + elif service.service == SERVICE_START_FLOW: + params[ATTR_TRANSITIONS] = \ + _transitions_config_parser(params[ATTR_TRANSITIONS]) + target_device.start_flow(**params) + + service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_MODE): + vol.In([mode.name.lower() for mode in PowerMode]) + }) + hass.services.register( + DOMAIN, SERVICE_SET_MODE, service_handler, + schema=service_schema_set_mode) + + service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend( + YEELIGHT_FLOW_TRANSITION_SCHEMA + ) + hass.services.register( + DOMAIN, SERVICE_START_FLOW, service_handler, + schema=service_schema_start_flow) + + for ipaddr, device_config in conf[CONF_DEVICES].items(): + _LOGGER.debug("Adding configured %s", device_config[CONF_NAME]) + _setup_device(hass, config, ipaddr, device_config) + + return True + + +def _setup_device(hass, hass_config, ipaddr, device_config): + devices = hass.data[DATA_YEELIGHT][CONF_DEVICES] + + if ipaddr in devices: + return + + device = YeelightDevice(hass, ipaddr, device_config) + + devices[ipaddr] = device + + platform_config = device_config.copy() + platform_config[CONF_HOST] = ipaddr + platform_config[CONF_CUSTOM_EFFECTS] = _parse_custom_effects( + hass_config[DATA_YEELIGHT].get(CONF_CUSTOM_EFFECTS, {}) + ) + + load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, hass_config) + + +class YeelightDevice: + """Represents single Yeelight device.""" + + def __init__(self, hass, ipaddr, config): + """Initialize device.""" + 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 + + @property + def bulb(self): + """Return bulb device.""" + import yeelight + if self._bulb_device is None: + try: + self._bulb_device = yeelight.Bulb(self._ipaddr, + model=self._model) + # force init for type + self._update_properties() + + except yeelight.BulbException as ex: + _LOGGER.error("Failed to connect to bulb %s, %s: %s", + self._ipaddr, self._name, ex) + + return self._bulb_device + + def _update_properties(self): + self._bulb_device.get_properties(UPDATE_REQUEST_PROPERTIES) + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def config(self): + """Return device config.""" + return self._config + + @property + def ipaddr(self): + """Return ip address.""" + return self._ipaddr + + @property + def is_nightlight_enabled(self) -> bool: + """Return true / false if nightlight is currently enabled.""" + if self._bulb_device is None: + return False + + return self.bulb.last_properties.get('active_mode') == '1' + + def turn_on(self, duration=DEFAULT_TRANSITION): + """Turn on device.""" + import yeelight + + try: + self._bulb_device.turn_on(duration=duration) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to turn the bulb on: %s", ex) + return + + self.update() + + def turn_off(self, duration=DEFAULT_TRANSITION): + """Turn off device.""" + import yeelight + + try: + self._bulb_device.turn_off(duration=duration) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to turn the bulb on: %s", ex) + return + + self.update() + + def update(self): + """Read new properties from the device.""" + if not self.bulb: + return + + self._update_properties() + dispatcher_send(self._hass, DATA_UPDATED, self._ipaddr) + + def set_mode(self, mode: str): + """Set a power mode.""" + import yeelight + + try: + self.bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()]) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to set the power mode: %s", ex) + + self.update() + + def start_flow(self, transitions, count=0, action=ACTION_RECOVER): + """Start flow.""" + import yeelight + + try: + flow = yeelight.Flow( + count=count, + action=yeelight.Flow.actions[action], + transitions=transitions) + + self.bulb.start_flow(flow) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to set effect: %s", ex) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 18a0bf750a1..8c7a94d3020 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,99 +1,26 @@ -""" -Support for Xiaomi Yeelight Wifi color bulb. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.yeelight/ -""" +"""Light platform support for yeelight.""" import logging -import voluptuous as vol - +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired) -from homeassistant.const import CONF_DEVICES, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_DEVICES, CONF_LIGHTS +from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, - SUPPORT_EFFECT, Light, PLATFORM_SCHEMA, ATTR_ENTITY_ID, DOMAIN) -import homeassistant.helpers.config_validation as cv + SUPPORT_EFFECT, Light) import homeassistant.util.color as color_util +from homeassistant.components.yeelight import ( + CONF_TRANSITION, DATA_YEELIGHT, CONF_MODE_MUSIC, + CONF_SAVE_ON_CHANGE, CONF_CUSTOM_EFFECTS, DATA_UPDATED) -REQUIREMENTS = ['yeelight==0.4.3'] +DEPENDENCIES = ['yeelight'] _LOGGER = logging.getLogger(__name__) -LEGACY_DEVICE_TYPE_MAP = { - 'color1': 'rgb', - 'mono1': 'white', - 'strip1': 'strip', - 'bslamp1': 'bedside', - 'ceiling1': 'ceiling', -} - -DEFAULT_NAME = 'Yeelight' -DEFAULT_TRANSITION = 350 - -CONF_MODEL = 'model' -CONF_TRANSITION = 'transition' -CONF_SAVE_ON_CHANGE = 'save_on_change' -CONF_MODE_MUSIC = 'use_music_mode' -CONF_CUSTOM_EFFECTS = 'custom_effects' -CONF_FLOW_PARAMS = 'flow_params' - -DATA_KEY = 'light.yeelight' - -ATTR_MODE = 'mode' -ATTR_COUNT = 'count' -ATTR_ACTION = 'action' -ATTR_TRANSITIONS = 'transitions' - -ACTION_RECOVER = 'recover' -ACTION_STAY = 'stay' -ACTION_OFF = 'off' - -YEELIGHT_RGB_TRANSITION = 'RGBTransition' -YEELIGHT_HSV_TRANSACTION = 'HSVTransition' -YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition' -YEELIGHT_SLEEP_TRANSACTION = 'SleepTransition' - -YEELIGHT_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, -}) - -YEELIGHT_FLOW_TRANSITION_SCHEMA = { - vol.Optional(ATTR_COUNT, default=0): cv.positive_int, - vol.Optional(ATTR_ACTION, default=ACTION_RECOVER): - vol.Any(ACTION_RECOVER, ACTION_OFF, ACTION_STAY), - vol.Required(ATTR_TRANSITIONS): [{ - vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION): - vol.All(cv.ensure_list, [cv.positive_int]), - vol.Exclusive(YEELIGHT_HSV_TRANSACTION, CONF_TRANSITION): - vol.All(cv.ensure_list, [cv.positive_int]), - vol.Exclusive(YEELIGHT_TEMPERATURE_TRANSACTION, CONF_TRANSITION): - vol.All(cv.ensure_list, [cv.positive_int]), - vol.Exclusive(YEELIGHT_SLEEP_TRANSACTION, CONF_TRANSITION): - vol.All(cv.ensure_list, [cv.positive_int]), - }] -} - -DEVICE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, - vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, - vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean, - vol.Optional(CONF_MODEL): cv.string, -}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, - vol.Optional(CONF_CUSTOM_EFFECTS): [{ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA - }] -}) - SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH) @@ -143,9 +70,6 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_TWITTER, EFFECT_STOP] -SERVICE_SET_MODE = 'yeelight_set_mode' -SERVICE_START_FLOW = 'yeelight_start_flow' - def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" @@ -160,117 +84,39 @@ def _cmd(func): return _wrap -def _parse_custom_effects(effects_config): - import yeelight - - effects = {} - for config in effects_config: - params = config[CONF_FLOW_PARAMS] - action = yeelight.Flow.actions[params[ATTR_ACTION]] - transitions = YeelightLight.transitions_config_parser( - params[ATTR_TRANSITIONS]) - - effects[config[CONF_NAME]] = { - ATTR_COUNT: params[ATTR_COUNT], - ATTR_ACTION: action, - ATTR_TRANSITIONS: transitions - } - - return effects - - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight bulbs.""" - from yeelight.enums import PowerMode - - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} + if not discovery_info: + return - lights = [] - if discovery_info is not None: - _LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) + yeelight_data = hass.data[DATA_YEELIGHT] + ipaddr = discovery_info[CONF_HOST] + device = yeelight_data[CONF_DEVICES][ipaddr] + _LOGGER.debug("Adding %s", device.name) - device_type = discovery_info['device_type'] - legacy_device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, - device_type) + custom_effects = discovery_info[CONF_CUSTOM_EFFECTS] + light = YeelightLight(device, custom_effects=custom_effects) - # Not using hostname, as it seems to vary. - name = "yeelight_%s_%s" % (legacy_device_type, - discovery_info['properties']['mac']) - device = {'name': name, 'ipaddr': discovery_info['host']} - - light = YeelightLight(device, DEVICE_SCHEMA({CONF_MODEL: device_type})) - lights.append(light) - hass.data[DATA_KEY][name] = light - else: - for ipaddr, device_config in config[CONF_DEVICES].items(): - name = device_config[CONF_NAME] - _LOGGER.debug("Adding configured %s", name) - - device = {'name': name, 'ipaddr': ipaddr} - - if CONF_CUSTOM_EFFECTS in config: - custom_effects = \ - _parse_custom_effects(config[CONF_CUSTOM_EFFECTS]) - else: - custom_effects = None - - light = YeelightLight(device, device_config, - custom_effects=custom_effects) - lights.append(light) - hass.data[DATA_KEY][name] = light - - add_entities(lights, True) - - def service_handler(service): - """Dispatch service calls to target entities.""" - params = {key: value for key, value in service.data.items() - if key != ATTR_ENTITY_ID} - entity_ids = service.data.get(ATTR_ENTITY_ID) - target_devices = [dev for dev in hass.data[DATA_KEY].values() - if dev.entity_id in entity_ids] - - for target_device in target_devices: - if service.service == SERVICE_SET_MODE: - target_device.set_mode(**params) - elif service.service == SERVICE_START_FLOW: - target_device.start_flow(**params) - - service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_MODE): - vol.In([mode.name.lower() for mode in PowerMode]) - }) - hass.services.register( - DOMAIN, SERVICE_SET_MODE, service_handler, - schema=service_schema_set_mode) - - service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend( - YEELIGHT_FLOW_TRANSITION_SCHEMA - ) - hass.services.register( - DOMAIN, SERVICE_START_FLOW, service_handler, - schema=service_schema_start_flow) + yeelight_data[CONF_LIGHTS][ipaddr] = light + add_entities([light], True) class YeelightLight(Light): """Representation of a Yeelight light.""" - def __init__(self, device, config, custom_effects=None): + def __init__(self, device, custom_effects=None): """Initialize the Yeelight light.""" - self.config = config - self._name = device['name'] - self._ipaddr = device['ipaddr'] + self.config = device.config + self._device = device self._supported_features = SUPPORT_YEELIGHT self._available = False - self._bulb_device = None self._brightness = None self._color_temp = None self._is_on = None self._hs = None - self._model = config.get('model') self._min_mireds = None self._max_mireds = None @@ -279,6 +125,22 @@ class YeelightLight(Light): else: self._custom_effects = {} + @callback + def _schedule_immediate_update(self, ipaddr): + if ipaddr == self.device.ipaddr: + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @property + def should_poll(self): + """No polling needed.""" + return False + @property def available(self) -> bool: """Return if bulb is available.""" @@ -302,7 +164,7 @@ class YeelightLight(Light): @property def name(self) -> str: """Return the name of the device if any.""" - return self._name + return self.device.name @property def is_on(self) -> bool: @@ -363,27 +225,26 @@ class YeelightLight(Light): @property def _properties(self) -> dict: - if self._bulb_device is None: + if self._bulb is None: return {} - return self._bulb_device.last_properties + return self._bulb.last_properties + + @property + def device(self): + """Return yeelight device.""" + return self._device # F821: https://github.com/PyCQA/pyflakes/issues/373 @property def _bulb(self) -> 'yeelight.Bulb': # noqa: F821 - import yeelight - if self._bulb_device is None: - try: - self._bulb_device = yeelight.Bulb(self._ipaddr, - model=self._model) - self._bulb_device.get_properties() # force init for type + bulb = self.device.bulb - 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) + if bulb: + self._available = True + return bulb - return self._bulb_device + self._available = False + return None def set_music_mode(self, mode) -> None: """Set the music mode on or off.""" @@ -396,12 +257,13 @@ class YeelightLight(Light): """Update properties from the bulb.""" import yeelight try: - self._bulb.get_properties() - - if self._bulb_device.bulb_type == yeelight.BulbType.Color: + if self._bulb.bulb_type == yeelight.BulbType.Color: self._supported_features = SUPPORT_YEELIGHT_RGB - elif self._bulb_device.bulb_type == yeelight.BulbType.WhiteTemp: - self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP + elif self._bulb.bulb_type == yeelight.BulbType.WhiteTemp: + if self._device.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() @@ -412,7 +274,11 @@ class YeelightLight(Light): self._is_on = self._properties.get('power') == 'on' - bright = self._properties.get('bright', None) + if self._device.is_nightlight_enabled: + bright = self._properties.get('nl_br', None) + else: + bright = self._properties.get('bright', None) + if bright: self._brightness = round(255 * (int(bright) / 100)) @@ -552,11 +418,7 @@ class YeelightLight(Light): if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - try: - self._bulb.turn_on(duration=duration) - except yeelight.BulbException as ex: - _LOGGER.error("Unable to turn the bulb on: %s", ex) - return + self.device.turn_on(duration=duration) if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: try: @@ -588,46 +450,8 @@ class YeelightLight(Light): def turn_off(self, **kwargs) -> None: """Turn off.""" - import yeelight duration = int(self.config[CONF_TRANSITION]) # in ms if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - try: - self._bulb.turn_off(duration=duration) - except yeelight.BulbException as ex: - _LOGGER.error("Unable to turn the bulb off: %s", ex) - - def set_mode(self, mode: str): - """Set a power mode.""" - import yeelight - try: - self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()]) - self.async_schedule_update_ha_state(True) - except yeelight.BulbException as ex: - _LOGGER.error("Unable to set the power mode: %s", ex) - - @staticmethod - def transitions_config_parser(transitions): - """Parse transitions config into initialized objects.""" - import yeelight - - transition_objects = [] - for transition_config in transitions: - transition, params = list(transition_config.items())[0] - transition_objects.append(getattr(yeelight, transition)(*params)) - return transition_objects - - def start_flow(self, transitions, count=0, action=ACTION_RECOVER): - """Start flow.""" - import yeelight - - try: - flow = yeelight.Flow( - count=count, - action=yeelight.Flow.actions[action], - transitions=self.transitions_config_parser(transitions)) - - self._bulb.start_flow(flow) - except yeelight.BulbException as ex: - _LOGGER.error("Unable to set effect: %s", ex) + self.device.turn_off(duration=duration) diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml new file mode 100644 index 00000000000..14dcfb27a4d --- /dev/null +++ b/homeassistant/components/yeelight/services.yaml @@ -0,0 +1,25 @@ +set_mode: + description: Set a operation mode. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + mode: + description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. + example: 'moonlight' + +start_flow: + description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + count: + description: The number of times to run this flow (0 to run forever). + example: 0 + action: + description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') + example: 'stay' + transitions: + description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html + example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' diff --git a/requirements_all.txt b/requirements_all.txt index cd74cbc0dd4..ea91ef5e9f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1813,7 +1813,7 @@ yahooweather==0.10 # homeassistant.components.yale_smart_alarm.alarm_control_panel yalesmartalarmclient==0.1.6 -# homeassistant.components.yeelight.light +# homeassistant.components.yeelight yeelight==0.4.3 # homeassistant.components.yeelightsunflower.light -- GitLab