diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 3507c6d2cda..9836bf97f90 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -192,3 +192,16 @@ yeelight_set_mode: 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 + 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/light/yeelight.py b/homeassistant/components/light/yeelight.py index 25704eea0cc..249f542325f 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -39,15 +39,48 @@ 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_TRANSITIONS = 'transitions' + +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.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, + vol.Optional(CONF_CUSTOM_EFFECTS): [{ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA + }] }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -103,11 +136,7 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_STOP] SERVICE_SET_MODE = 'yeelight_set_mode' -ATTR_MODE = 'mode' - -YEELIGHT_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) +SERVICE_START_FLOW = 'yeelight_start_flow' def _cmd(func): @@ -123,6 +152,19 @@ def _cmd(func): return _wrap +def _parse_custom_effects(effects_config): + effects = {} + for config in effects_config: + params = config[CONF_FLOW_PARAMS] + transitions = YeelightLight.transitions_config_parser( + params[ATTR_TRANSITIONS]) + + effects[config[CONF_NAME]] = \ + {ATTR_COUNT: params[ATTR_COUNT], 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 @@ -151,8 +193,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = device_config[CONF_NAME] _LOGGER.debug("Adding configured %s", name) + custom_effects = _parse_custom_effects(config[CONF_CUSTOM_EFFECTS]) device = {'name': name, 'ipaddr': ipaddr} - light = YeelightLight(device, device_config) + light = YeelightLight(device, device_config, + custom_effects=custom_effects) lights.append(light) hass.data[DATA_KEY][name] = light @@ -163,15 +207,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): params = {key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID} entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - target_devices = [dev for dev in hass.data[DATA_KEY].values() - if dev.entity_id in entity_ids] - else: - target_devices = hass.data[DATA_KEY].values() + 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): @@ -181,11 +224,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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) + class YeelightLight(Light): """Representation of a Yeelight light.""" - def __init__(self, device, config): + def __init__(self, device, config, custom_effects=None): """Initialize the Yeelight light.""" self.config = config self._name = device['name'] @@ -204,6 +254,11 @@ class YeelightLight(Light): self._min_mireds = None self._max_mireds = None + if custom_effects: + self._custom_effects = custom_effects + else: + self._custom_effects = {} + @property def available(self) -> bool: """Return if bulb is available.""" @@ -217,7 +272,7 @@ class YeelightLight(Light): @property def effect_list(self): """Return the list of supported effects.""" - return YEELIGHT_EFFECT_LIST + return YEELIGHT_EFFECT_LIST + self.custom_effects_names @property def color_temp(self) -> int: @@ -249,6 +304,16 @@ class YeelightLight(Light): """Return maximum supported color temperature.""" return self._max_mireds + @property + def custom_effects(self): + """Return dict with custom effects.""" + return self._custom_effects + + @property + def custom_effects_names(self): + """Return list with custom effects names.""" + return list(self.custom_effects.keys()) + def _get_hs_from_properties(self): rgb = self._properties.get('rgb', None) color_mode = self._properties.get('color_mode', None) @@ -435,15 +500,17 @@ class YeelightLight(Light): EFFECT_SLOWDOWN: slowdown, } - if effect in effects_map: + if effect in self.custom_effects_names: + flow = Flow(**self.custom_effects[effect]) + elif effect in effects_map: flow = Flow(count=0, transitions=effects_map[effect]()) - if effect == EFFECT_FAST_RANDOM_LOOP: + elif effect == EFFECT_FAST_RANDOM_LOOP: flow = Flow(count=0, transitions=randomloop(duration=250)) - if effect == EFFECT_WHATSAPP: + elif effect == EFFECT_WHATSAPP: flow = Flow(count=2, transitions=pulse(37, 211, 102)) - if effect == EFFECT_FACEBOOK: + elif effect == EFFECT_FACEBOOK: flow = Flow(count=2, transitions=pulse(59, 89, 152)) - if effect == EFFECT_TWITTER: + elif effect == EFFECT_TWITTER: flow = Flow(count=2, transitions=pulse(0, 172, 237)) try: @@ -518,3 +585,28 @@ class YeelightLight(Light): 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): + """Start flow.""" + import yeelight + + try: + flow = yeelight.Flow( + count=count, + transitions=self.transitions_config_parser(transitions)) + + self._bulb.start_flow(flow) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to set effect: %s", ex)