From aeae0feac175d71e7435c92390ced5220ebd1bbc Mon Sep 17 00:00:00 2001 From: zewelor Date: Sat, 2 Mar 2019 14:22:52 +0100 Subject: [PATCH 1/5] Move yeelight into component and add power mode switch --- .../components/discovery/__init__.py | 3 +- homeassistant/components/yeelight/__init__.py | 225 +++++++++++++++++- homeassistant/components/yeelight/light.py | 222 +++++------------ homeassistant/components/yeelight/switch.py | 64 +++++ 4 files changed, 345 insertions(+), 169 deletions(-) create mode 100644 homeassistant/components/yeelight/switch.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index d4816213f50085..1fb727642bc50d 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -46,6 +46,7 @@ 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_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 @@ '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/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index d8c1f23bcbb2a5..6a0e99dbd78331 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1 +1,224 @@ -"""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 netdisco.const import ATTR_DEVICE_TYPE, ATTR_HOSTNAME, ATTR_PROPERTIES + +from homeassistant.components.discovery import SERVICE_YEELIGHT +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_SCAN_INTERVAL, \ + CONF_LIGHTS, CONF_HOST +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.helpers import discovery +from homeassistant.helpers.discovery import load_platform +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['yeelight==0.4.3'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "yeelight" +DATA_YEELIGHT = 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' + +LEGACY_DEVICE_TYPE_MAP = { + 'color1': 'rgb', + 'mono1': 'white', + 'strip1': 'strip', + 'bslamp1': 'bedside', + 'ceiling1': 'ceiling', +} + + +ATTR_MODE = 'mode' +ATTR_COUNT = 'count' +ATTR_ACTION = 'action' +ATTR_TRANSITIONS = 'transitions' + +ACTION_RECOVER = 'recover' +ACTION_STAY = 'stay' +ACTION_OFF = 'off' + +SCAN_INTERVAL = timedelta(seconds=30) + +YEELIGHT_RGB_TRANSITION = 'RGBTransition' +YEELIGHT_HSV_TRANSACTION = 'HSVTransition' +YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition' +YEELIGHT_SLEEP_TRANSACTION = 'SleepTransition' + +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) + +NIGHTLIGHT_SUPPORTED_MODELS = [ + "ceiling3", + 'ceiling4' +] + +UPDATE_REQUEST_PROPERTIES = [ + "power", + "bright", + "ct", + "rgb", + "hue", + "sat", + "color_mode", + "flowing", + "music_on", + "nl_br", + "active_mode", +] + + +def setup(hass, config): + """Set up the Yeelight bulbs.""" + + conf = config[DOMAIN] + hass.data[DATA_YEELIGHT] = { + CONF_DEVICES: {}, + CONF_LIGHTS: {}, + } + + def device_discovered(service, info): + _LOGGER.debug("Adding autodetected %s", info[ATTR_HOSTNAME]) + + device_type = info[ATTR_DEVICE_TYPE] + legacy_device_type = \ + LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type) + + name = "yeelight_%s_%s" % (legacy_device_type, + info[ATTR_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) + + 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(ipaddr, device_config) + devices[ipaddr] = device + + platform_config = device_config.copy() + platform_config[CONF_HOST] = ipaddr + platform_config[CONF_CUSTOM_EFFECTS] = \ + hass_config[DATA_YEELIGHT].get(CONF_CUSTOM_EFFECTS, {}) + + load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, hass_config) + + if device.is_nightlight_supported: + load_platform(hass, SWITCH_DOMAIN, DOMAIN, platform_config, + hass_config) + + +class YeelightDevice: + """Represents single Yeelight device""" + + def __init__(self, ipaddr, config): + 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._bulb_device.get_properties(UPDATE_REQUEST_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 + + @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 is_nightlight_supported(self): + """Return true / false if nightlight is supported""" + + return self._model in NIGHTLIGHT_SUPPORTED_MODELS + + def update(self): + """Read new properties from the device""" + if self.bulb: + return self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) + + return {} diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 18a0bf750a1351..758195ade84bda 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,9 +1,4 @@ -""" -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 @@ -11,89 +6,23 @@ 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_NAME, CONF_DEVICES, CONF_LIGHTS, CONF_HOST 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) + SUPPORT_EFFECT, Light, ATTR_ENTITY_ID, DOMAIN) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util +from homeassistant.components.yeelight import ( + CONF_TRANSITION, CONF_FLOW_PARAMS, DATA_YEELIGHT, CONF_MODE_MUSIC, + CONF_SAVE_ON_CHANGE, ACTION_RECOVER, ATTR_TRANSITIONS, ATTR_COUNT, + CONF_CUSTOM_EFFECTS, YEELIGHT_FLOW_TRANSITION_SCHEMA) -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) @@ -106,6 +35,10 @@ SUPPORT_EFFECT | SUPPORT_COLOR_TEMP) +YEELIGHT_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + EFFECT_DISCO = "Disco" EFFECT_TEMP = "Slow Temp" EFFECT_STROBE = "Strobe epilepsy!" @@ -143,7 +76,6 @@ EFFECT_TWITTER, EFFECT_STOP] -SERVICE_SET_MODE = 'yeelight_set_mode' SERVICE_START_FLOW = 'yeelight_start_flow' @@ -160,6 +92,18 @@ def _wrap(self, *args, **kwargs): return _wrap +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 @@ -167,7 +111,7 @@ def _parse_custom_effects(effects_config): for config in effects_config: params = config[CONF_FLOW_PARAMS] action = yeelight.Flow.actions[params[ATTR_ACTION]] - transitions = YeelightLight.transitions_config_parser( + transitions = _transitions_config_parser( params[ATTR_TRANSITIONS]) effects[config[CONF_NAME]] = { @@ -181,69 +125,34 @@ def _parse_custom_effects(effects_config): 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] = {} - - lights = [] - if discovery_info is not None: - _LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) + if not discovery_info: + return - device_type = discovery_info['device_type'] - legacy_device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, - device_type) + yeelight_data = hass.data[DATA_YEELIGHT] + ipaddr = discovery_info[CONF_HOST] + device = yeelight_data[CONF_DEVICES][ipaddr] + _LOGGER.debug("Adding %s", device.name) - # 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']} + custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS]) + light = YeelightLight(device, custom_effects=custom_effects) - 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) + yeelight_data[CONF_LIGHTS][ipaddr] = light + add_entities([light], 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() + target_devices = [dev 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: + if 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 ) @@ -255,22 +164,19 @@ def service_handler(service): 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 @@ -302,7 +208,7 @@ def color_temp(self) -> int: @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 +269,21 @@ def hs_color(self) -> tuple: @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 # 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,11 +296,11 @@ def update(self) -> None: """Update properties from the bulb.""" import yeelight try: - self._bulb.get_properties() + self._device.update() - 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: + elif self._bulb.bulb_type == yeelight.BulbType.WhiteTemp: self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP if self._min_mireds is None: @@ -606,18 +506,6 @@ def set_mode(self, mode: str): 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 @@ -626,7 +514,7 @@ def start_flow(self, transitions, count=0, action=ACTION_RECOVER): flow = yeelight.Flow( count=count, action=yeelight.Flow.actions[action], - transitions=self.transitions_config_parser(transitions)) + transitions=transitions) self._bulb.start_flow(flow) except yeelight.BulbException as ex: diff --git a/homeassistant/components/yeelight/switch.py b/homeassistant/components/yeelight/switch.py new file mode 100644 index 00000000000000..1419d60687699e --- /dev/null +++ b/homeassistant/components/yeelight/switch.py @@ -0,0 +1,64 @@ +"""Switch platform support for yeelight""" +import logging + +from netdisco.const import ATTR_HOST + +from homeassistant.const import CONF_DEVICES +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.yeelight import DATA_YEELIGHT + +DEPENDENCIES = ['yeelight'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Yeelight switches.""" + + if not discovery_info: + return + + device = hass.data[DATA_YEELIGHT][CONF_DEVICES][discovery_info[ATTR_HOST]] + + _LOGGER.debug("Adding power mode switch for %s", device.name) + + add_entities([YeelightPowerModeSwitch(device)]) + return True + + +class YeelightPowerModeSwitch(ToggleEntity): + """Representation of a Yeelight power mode switch for night / moon light""" + + def __init__(self, device): + self._device = device + + @property + def is_on(self) -> bool: + return self._bulb.last_properties.get('active_mode') == '1' + + @property + def name(self): + """Return the name of the sensor.""" + return "{} night light".format(self._device.name) + + @property + def _bulb(self): + return self._device.bulb + + @property + def icon(self): + return 'mdi:weather-night' + + def turn_on(self, **kwargs) -> None: + import yeelight + + self._bulb.set_power_mode(yeelight.enums.PowerMode.MOONLIGHT) + self._device.update() + self.async_schedule_update_ha_state(True) + + def turn_off(self, **kwargs) -> None: + import yeelight + + self._bulb.set_power_mode(yeelight.enums.PowerMode.NORMAL) + self._device.update() + self.async_schedule_update_ha_state(True) From 124f0475fd3a5e14e6f7d9e56bfca581f029eda5 Mon Sep 17 00:00:00 2001 From: zewelor Date: Sun, 3 Mar 2019 10:37:11 +0100 Subject: [PATCH 2/5] Synchronize state between entities, fixes for nightlight --- homeassistant/components/yeelight/__init__.py | 97 ++++++++++++++----- homeassistant/components/yeelight/light.py | 54 ++++++----- homeassistant/components/yeelight/switch.py | 36 ++++--- 3 files changed, 128 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 6a0e99dbd78331..e54a0a51ecfc2e 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -8,8 +8,6 @@ from datetime import timedelta import voluptuous as vol -from netdisco.const import ATTR_DEVICE_TYPE, ATTR_HOSTNAME, ATTR_PROPERTIES - from homeassistant.components.discovery import SERVICE_YEELIGHT from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_SCAN_INTERVAL, \ CONF_LIGHTS, CONF_HOST @@ -18,6 +16,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.event import async_track_time_interval REQUIREMENTS = ['yeelight==0.4.3'] @@ -25,6 +25,7 @@ DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN +DATA_UPDATED = '{}_data_updated'.format(DOMAIN) DEFAULT_NAME = 'Yeelight' DEFAULT_TRANSITION = 350 @@ -36,15 +37,6 @@ CONF_FLOW_PARAMS = 'flow_params' CONF_CUSTOM_EFFECTS = 'custom_effects' -LEGACY_DEVICE_TYPE_MAP = { - 'color1': 'rgb', - 'mono1': 'white', - 'strip1': 'strip', - 'bslamp1': 'bedside', - 'ceiling1': 'ceiling', -} - - ATTR_MODE = 'mode' ATTR_COUNT = 'count' ATTR_ACTION = 'action' @@ -54,6 +46,9 @@ ACTION_STAY = 'stay' ACTION_OFF = 'off' +MODE_MOONLIGHT = 'moonlight' +MODE_DAYLIGHT = 'normal' + SCAN_INTERVAL = timedelta(seconds=30) YEELIGHT_RGB_TRANSITION = 'RGBTransition' @@ -127,14 +122,12 @@ def setup(hass, config): } def device_discovered(service, info): - _LOGGER.debug("Adding autodetected %s", info[ATTR_HOSTNAME]) + _LOGGER.debug("Adding autodetected %s", info['hostname']) - device_type = info[ATTR_DEVICE_TYPE] - legacy_device_type = \ - LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type) + device_type = info['device_type'] - name = "yeelight_%s_%s" % (legacy_device_type, - info[ATTR_PROPERTIES]['mac']) + name = "yeelight_%s_%s" % (device_type, + info['properties']['mac']) ipaddr = info[CONF_HOST] device_config = DEVICE_SCHEMA({ CONF_NAME: name, @@ -145,6 +138,14 @@ def device_discovered(service, info): discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) + def async_update(event): + for device in hass.data[DATA_YEELIGHT][CONF_DEVICES].values(): + device.update() + + async_track_time_interval( + hass, async_update, conf[CONF_SCAN_INTERVAL] + ) + 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) @@ -158,7 +159,8 @@ def _setup_device(hass, hass_config, ipaddr, device_config): if ipaddr in devices: return - device = YeelightDevice(ipaddr, device_config) + device = YeelightDevice(hass, ipaddr, device_config) + devices[ipaddr] = device platform_config = device_config.copy() @@ -176,7 +178,8 @@ def _setup_device(hass, hass_config, ipaddr, device_config): class YeelightDevice: """Represents single Yeelight device""" - def __init__(self, ipaddr, config): + def __init__(self, hass, ipaddr, config): + self._hass = hass self._config = config self._ipaddr = ipaddr self._name = config.get(CONF_NAME) @@ -192,7 +195,7 @@ def bulb(self): self._bulb_device = yeelight.Bulb(self._ipaddr, model=self._model) # force init for type - self._bulb_device.get_properties(UPDATE_REQUEST_PROPERTIES) + self._update_properties() except yeelight.BulbException as ex: _LOGGER.error("Failed to connect to bulb %s, %s: %s", @@ -211,14 +214,60 @@ def config(self): return self._config @property - def is_nightlight_supported(self): + def ipaddr(self): + """Return ip address""" + return self._ipaddr + + @property + def is_nightlight_supported(self) -> bool: """Return true / false if nightlight is supported""" return self._model in NIGHTLIGHT_SUPPORTED_MODELS + @property + def is_nightlight_enabled(self) -> bool: + """Return true / false if nightlight is currently enabled""" + 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 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 update(self): """Read new properties from the device""" - if self.bulb: - return self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) + if not self.bulb: + return + + self._update_properties() + dispatcher_send(self._hass, DATA_UPDATED, self._ipaddr) - return {} + def _update_properties(self): + self._bulb_device.get_properties(UPDATE_REQUEST_PROPERTIES) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 758195ade84bda..7e729e11bda9ab 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -3,10 +3,12 @@ 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_NAME, CONF_DEVICES, CONF_LIGHTS, CONF_HOST +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, @@ -17,7 +19,7 @@ from homeassistant.components.yeelight import ( CONF_TRANSITION, CONF_FLOW_PARAMS, DATA_YEELIGHT, CONF_MODE_MUSIC, CONF_SAVE_ON_CHANGE, ACTION_RECOVER, ATTR_TRANSITIONS, ATTR_COUNT, - CONF_CUSTOM_EFFECTS, YEELIGHT_FLOW_TRANSITION_SCHEMA) + CONF_CUSTOM_EFFECTS, YEELIGHT_FLOW_TRANSITION_SCHEMA, DATA_UPDATED) DEPENDENCIES = ['yeelight'] @@ -185,6 +187,23 @@ def __init__(self, device, custom_effects=None): 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.""" @@ -296,12 +315,13 @@ def update(self) -> None: """Update properties from the bulb.""" import yeelight try: - self._device.update() - if self._bulb.bulb_type == yeelight.BulbType.Color: self._supported_features = SUPPORT_YEELIGHT_RGB elif self._bulb.bulb_type == yeelight.BulbType.WhiteTemp: - self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP + 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() @@ -312,7 +332,11 @@ def update(self) -> None: 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)) @@ -452,11 +476,7 @@ def turn_on(self, **kwargs) -> None: 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: @@ -488,23 +508,11 @@ def turn_on(self, **kwargs) -> None: 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) + self._device.turn_off(duration=duration) def start_flow(self, transitions, count=0, action=ACTION_RECOVER): """Start flow.""" diff --git a/homeassistant/components/yeelight/switch.py b/homeassistant/components/yeelight/switch.py index 1419d60687699e..f60ec6042b6b1e 100644 --- a/homeassistant/components/yeelight/switch.py +++ b/homeassistant/components/yeelight/switch.py @@ -4,8 +4,11 @@ from netdisco.const import ATTR_HOST from homeassistant.const import CONF_DEVICES +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity -from homeassistant.components.yeelight import DATA_YEELIGHT +from homeassistant.components.yeelight import DATA_YEELIGHT, MODE_MOONLIGHT, \ + MODE_DAYLIGHT, DATA_UPDATED DEPENDENCIES = ['yeelight'] @@ -32,9 +35,26 @@ class YeelightPowerModeSwitch(ToggleEntity): def __init__(self, device): self._device = device + @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 is_on(self) -> bool: - return self._bulb.last_properties.get('active_mode') == '1' + return self._device.is_nightlight_enabled @property def name(self): @@ -50,15 +70,7 @@ def icon(self): return 'mdi:weather-night' def turn_on(self, **kwargs) -> None: - import yeelight - - self._bulb.set_power_mode(yeelight.enums.PowerMode.MOONLIGHT) - self._device.update() - self.async_schedule_update_ha_state(True) + self._device.set_mode(MODE_MOONLIGHT) def turn_off(self, **kwargs) -> None: - import yeelight - - self._bulb.set_power_mode(yeelight.enums.PowerMode.NORMAL) - self._device.update() - self.async_schedule_update_ha_state(True) + self._device.set_mode(MODE_DAYLIGHT) From f1e212f113e51a22c880993ac7817eadc38ea9c4 Mon Sep 17 00:00:00 2001 From: zewelor Date: Mon, 4 Mar 2019 17:35:08 +0100 Subject: [PATCH 3/5] Lint fixes --- homeassistant/components/yeelight/__init__.py | 20 ++++++++++--------- homeassistant/components/yeelight/light.py | 5 ++--- homeassistant/components/yeelight/switch.py | 17 ++++++++-------- requirements_all.txt | 2 +- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index e54a0a51ecfc2e..389e77fc9a07b7 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -114,7 +114,6 @@ def setup(hass, config): """Set up the Yeelight bulbs.""" - conf = config[DOMAIN] hass.data[DATA_YEELIGHT] = { CONF_DEVICES: {}, @@ -176,9 +175,10 @@ def _setup_device(hass, hass_config, ipaddr, device_config): class YeelightDevice: - """Represents single Yeelight device""" + """Represents single Yeelight device.""" def __init__(self, hass, ipaddr, config): + """Initialize device.""" self._hass = hass self._config = config self._ipaddr = ipaddr @@ -215,23 +215,23 @@ def config(self): @property def ipaddr(self): - """Return ip address""" + """Return ip address.""" return self._ipaddr @property def is_nightlight_supported(self) -> bool: - """Return true / false if nightlight is supported""" - + """Return true / false if nightlight is supported.""" return self._model in NIGHTLIGHT_SUPPORTED_MODELS @property def is_nightlight_enabled(self) -> bool: - """Return true / false if nightlight is currently enabled""" + """Return true / false if nightlight is currently enabled.""" return self.bulb.last_properties.get('active_mode') == '1' def turn_on(self, duration=DEFAULT_TRANSITION): - """Turn on device""" + """Turn on device.""" import yeelight + try: self._bulb_device.turn_on(duration=duration) except yeelight.BulbException as ex: @@ -241,8 +241,9 @@ def turn_on(self, duration=DEFAULT_TRANSITION): self.update() def turn_off(self, duration=DEFAULT_TRANSITION): - """Turn off device""" + """Turn off device.""" import yeelight + try: self._bulb_device.turn_off(duration=duration) except yeelight.BulbException as ex: @@ -254,6 +255,7 @@ def turn_off(self, duration=DEFAULT_TRANSITION): 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: @@ -262,7 +264,7 @@ def set_mode(self, mode: str): self.update() def update(self): - """Read new properties from the device""" + """Read new properties from the device.""" if not self.bulb: return diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 7e729e11bda9ab..929c393737b8cd 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,4 +1,4 @@ -"""Light platform support for yeelight""" +"""Light platform support for yeelight.""" import logging import voluptuous as vol @@ -194,14 +194,13 @@ def _schedule_immediate_update(self, ipaddr): 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""" + """No polling needed.""" return False @property diff --git a/homeassistant/components/yeelight/switch.py b/homeassistant/components/yeelight/switch.py index f60ec6042b6b1e..12efcf4d742114 100644 --- a/homeassistant/components/yeelight/switch.py +++ b/homeassistant/components/yeelight/switch.py @@ -1,8 +1,6 @@ -"""Switch platform support for yeelight""" +"""Switch platform support for yeelight.""" import logging -from netdisco.const import ATTR_HOST - from homeassistant.const import CONF_DEVICES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -17,11 +15,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight switches.""" - if not discovery_info: return - device = hass.data[DATA_YEELIGHT][CONF_DEVICES][discovery_info[ATTR_HOST]] + device = hass.data[DATA_YEELIGHT][CONF_DEVICES][discovery_info['host']] _LOGGER.debug("Adding power mode switch for %s", device.name) @@ -30,9 +27,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class YeelightPowerModeSwitch(ToggleEntity): - """Representation of a Yeelight power mode switch for night / moon light""" + """Representation of a Yeelight power mode switch for nightlight.""" def __init__(self, device): + """Itialize power mode switch.""" self._device = device @callback @@ -42,18 +40,18 @@ def _schedule_immediate_update(self, ipaddr): 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""" + """No polling needed.""" return False @property def is_on(self) -> bool: + """Return true if on.""" return self._device.is_nightlight_enabled @property @@ -67,10 +65,13 @@ def _bulb(self): @property def icon(self): + """Return the icon.""" return 'mdi:weather-night' def turn_on(self, **kwargs) -> None: + """Turn the nightlight on and turn off daylight.""" self._device.set_mode(MODE_MOONLIGHT) def turn_off(self, **kwargs) -> None: + """Turn the daylight on and turn off nightlight.""" self._device.set_mode(MODE_DAYLIGHT) diff --git a/requirements_all.txt b/requirements_all.txt index cd74cbc0dd483d..ea91ef5e9f40c8 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 From 1101c18e22baf432e5b5cb75fb464a68a0b60072 Mon Sep 17 00:00:00 2001 From: zewelor Date: Sun, 10 Mar 2019 18:49:56 +0100 Subject: [PATCH 4/5] Move yeelight services to component domain --- homeassistant/components/light/services.yaml | 26 ---- homeassistant/components/yeelight/__init__.py | 116 ++++++++++++++++-- homeassistant/components/yeelight/light.py | 101 +++------------ .../components/yeelight/services.yaml | 25 ++++ 4 files changed, 144 insertions(+), 124 deletions(-) create mode 100644 homeassistant/components/yeelight/services.yaml diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index a2863482477a9a..cdf82e97429ad2 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 389e77fc9a07b7..967eb792945635 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.discovery import SERVICE_YEELIGHT from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_SCAN_INTERVAL, \ - CONF_LIGHTS, CONF_HOST + CONF_HOST, ATTR_ENTITY_ID, CONF_LIGHTS from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.helpers import discovery @@ -18,6 +18,7 @@ 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'] @@ -56,6 +57,9 @@ 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): @@ -92,6 +96,10 @@ }), }, extra=vol.ALLOW_EXTRA) +YEELIGHT_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + NIGHTLIGHT_SUPPORTED_MODELS = [ "ceiling3", 'ceiling4' @@ -112,10 +120,43 @@ ] +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] - hass.data[DATA_YEELIGHT] = { + yeelight_data = hass.data[DATA_YEELIGHT] = { CONF_DEVICES: {}, CONF_LIGHTS: {}, } @@ -138,13 +179,46 @@ def device_discovered(service, info): discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) def async_update(event): - for device in hass.data[DATA_YEELIGHT][CONF_DEVICES].values(): + 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) @@ -164,8 +238,9 @@ def _setup_device(hass, hass_config, ipaddr, device_config): platform_config = device_config.copy() platform_config[CONF_HOST] = ipaddr - platform_config[CONF_CUSTOM_EFFECTS] = \ + 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) @@ -203,6 +278,9 @@ def bulb(self): 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.""" @@ -226,6 +304,9 @@ def is_nightlight_supported(self) -> bool: @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): @@ -252,6 +333,14 @@ def turn_off(self, duration=DEFAULT_TRANSITION): 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 @@ -263,13 +352,16 @@ def set_mode(self, mode: str): self.update() - def update(self): - """Read new properties from the device.""" - if not self.bulb: - return + def start_flow(self, transitions, count=0, action=ACTION_RECOVER): + """Start flow.""" + import yeelight - self._update_properties() - dispatcher_send(self._hass, DATA_UPDATED, self._ipaddr) + try: + flow = yeelight.Flow( + count=count, + action=yeelight.Flow.actions[action], + transitions=transitions) - def _update_properties(self): - self._bulb_device.get_properties(UPDATE_REQUEST_PROPERTIES) + 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 929c393737b8cd..8c7a94d3020659 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,25 +1,21 @@ """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_NAME, CONF_DEVICES, CONF_LIGHTS, CONF_HOST +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, 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, CONF_FLOW_PARAMS, DATA_YEELIGHT, CONF_MODE_MUSIC, - CONF_SAVE_ON_CHANGE, ACTION_RECOVER, ATTR_TRANSITIONS, ATTR_COUNT, - CONF_CUSTOM_EFFECTS, YEELIGHT_FLOW_TRANSITION_SCHEMA, DATA_UPDATED) + CONF_TRANSITION, DATA_YEELIGHT, CONF_MODE_MUSIC, + CONF_SAVE_ON_CHANGE, CONF_CUSTOM_EFFECTS, DATA_UPDATED) DEPENDENCIES = ['yeelight'] @@ -37,10 +33,6 @@ SUPPORT_EFFECT | SUPPORT_COLOR_TEMP) -YEELIGHT_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, -}) - EFFECT_DISCO = "Disco" EFFECT_TEMP = "Slow Temp" EFFECT_STROBE = "Strobe epilepsy!" @@ -78,8 +70,6 @@ EFFECT_TWITTER, EFFECT_STOP] -SERVICE_START_FLOW = 'yeelight_start_flow' - def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" @@ -94,37 +84,6 @@ def _wrap(self, *args, **kwargs): return _wrap -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_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight bulbs.""" if not discovery_info: @@ -135,33 +94,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device = yeelight_data[CONF_DEVICES][ipaddr] _LOGGER.debug("Adding %s", device.name) - custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS]) + custom_effects = discovery_info[CONF_CUSTOM_EFFECTS] light = YeelightLight(device, custom_effects=custom_effects) yeelight_data[CONF_LIGHTS][ipaddr] = light add_entities([light], 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 yeelight_data[CONF_LIGHTS].values() - if dev.entity_id in entity_ids] - - for target_device in target_devices: - if service.service == SERVICE_START_FLOW: - params[ATTR_TRANSITIONS] = \ - _transitions_config_parser(params[ATTR_TRANSITIONS]) - target_device.start_flow(**params) - - 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) - class YeelightLight(Light): """Representation of a Yeelight light.""" @@ -189,7 +127,7 @@ def __init__(self, device, custom_effects=None): @callback def _schedule_immediate_update(self, ipaddr): - if ipaddr == self._device.ipaddr: + if ipaddr == self.device.ipaddr: self.async_schedule_update_ha_state(True) async def async_added_to_hass(self): @@ -226,7 +164,7 @@ def color_temp(self) -> int: @property def name(self) -> str: """Return the name of the device if any.""" - return self._device.name + return self.device.name @property def is_on(self) -> bool: @@ -291,10 +229,15 @@ def _properties(self) -> dict: return {} 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 - bulb = self._device.bulb + bulb = self.device.bulb if bulb: self._available = True @@ -475,7 +418,7 @@ def turn_on(self, **kwargs) -> None: if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self._device.turn_on(duration=duration) + self.device.turn_on(duration=duration) if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: try: @@ -511,18 +454,4 @@ def turn_off(self, **kwargs) -> None: if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self._device.turn_off(duration=duration) - - 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) + 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 00000000000000..14dcfb27a4d54a --- /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] }]' From ff53476b87eb0a13a538faa90353e025c1eaa36b Mon Sep 17 00:00:00 2001 From: zewelor Date: Mon, 11 Mar 2019 21:58:52 +0100 Subject: [PATCH 5/5] Remove switch from PR --- homeassistant/components/yeelight/__init__.py | 10 --- homeassistant/components/yeelight/switch.py | 77 ------------------- 2 files changed, 87 deletions(-) delete mode 100644 homeassistant/components/yeelight/switch.py diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 967eb792945635..32e3c5f69e3b6f 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -12,7 +12,6 @@ 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.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.helpers import discovery from homeassistant.helpers.discovery import load_platform import homeassistant.helpers.config_validation as cv @@ -244,10 +243,6 @@ def _setup_device(hass, hass_config, ipaddr, device_config): load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, hass_config) - if device.is_nightlight_supported: - load_platform(hass, SWITCH_DOMAIN, DOMAIN, platform_config, - hass_config) - class YeelightDevice: """Represents single Yeelight device.""" @@ -296,11 +291,6 @@ def ipaddr(self): """Return ip address.""" return self._ipaddr - @property - def is_nightlight_supported(self) -> bool: - """Return true / false if nightlight is supported.""" - return self._model in NIGHTLIGHT_SUPPORTED_MODELS - @property def is_nightlight_enabled(self) -> bool: """Return true / false if nightlight is currently enabled.""" diff --git a/homeassistant/components/yeelight/switch.py b/homeassistant/components/yeelight/switch.py deleted file mode 100644 index 12efcf4d742114..00000000000000 --- a/homeassistant/components/yeelight/switch.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Switch platform support for yeelight.""" -import logging - -from homeassistant.const import CONF_DEVICES -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.components.yeelight import DATA_YEELIGHT, MODE_MOONLIGHT, \ - MODE_DAYLIGHT, DATA_UPDATED - -DEPENDENCIES = ['yeelight'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Yeelight switches.""" - if not discovery_info: - return - - device = hass.data[DATA_YEELIGHT][CONF_DEVICES][discovery_info['host']] - - _LOGGER.debug("Adding power mode switch for %s", device.name) - - add_entities([YeelightPowerModeSwitch(device)]) - return True - - -class YeelightPowerModeSwitch(ToggleEntity): - """Representation of a Yeelight power mode switch for nightlight.""" - - def __init__(self, device): - """Itialize power mode switch.""" - self._device = device - - @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 is_on(self) -> bool: - """Return true if on.""" - return self._device.is_nightlight_enabled - - @property - def name(self): - """Return the name of the sensor.""" - return "{} night light".format(self._device.name) - - @property - def _bulb(self): - return self._device.bulb - - @property - def icon(self): - """Return the icon.""" - return 'mdi:weather-night' - - def turn_on(self, **kwargs) -> None: - """Turn the nightlight on and turn off daylight.""" - self._device.set_mode(MODE_MOONLIGHT) - - def turn_off(self, **kwargs) -> None: - """Turn the daylight on and turn off nightlight.""" - self._device.set_mode(MODE_DAYLIGHT)