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