Add effects shipped by Yeelight in their apps, as these effects are quite popular and have been missing from Home Assistant.
985 lines
29 KiB
Python
985 lines
29 KiB
Python
"""Light platform support for yeelight."""
|
|
from functools import partial
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
import yeelight
|
|
from yeelight import (
|
|
BulbException,
|
|
Flow,
|
|
RGBTransition,
|
|
SleepTransition,
|
|
flows,
|
|
transitions as yee_transitions,
|
|
)
|
|
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
|
|
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS,
|
|
ATTR_COLOR_TEMP,
|
|
ATTR_EFFECT,
|
|
ATTR_FLASH,
|
|
ATTR_HS_COLOR,
|
|
ATTR_KELVIN,
|
|
ATTR_RGB_COLOR,
|
|
ATTR_TRANSITION,
|
|
FLASH_LONG,
|
|
FLASH_SHORT,
|
|
SUPPORT_BRIGHTNESS,
|
|
SUPPORT_COLOR,
|
|
SUPPORT_COLOR_TEMP,
|
|
SUPPORT_EFFECT,
|
|
SUPPORT_FLASH,
|
|
SUPPORT_TRANSITION,
|
|
LightEntity,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers import entity_platform
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
import homeassistant.util.color as color_util
|
|
from homeassistant.util.color import (
|
|
color_temperature_kelvin_to_mired as kelvin_to_mired,
|
|
color_temperature_mired_to_kelvin as mired_to_kelvin,
|
|
)
|
|
|
|
from . import (
|
|
ACTION_RECOVER,
|
|
ATTR_ACTION,
|
|
ATTR_COUNT,
|
|
ATTR_TRANSITIONS,
|
|
CONF_FLOW_PARAMS,
|
|
CONF_MODE_MUSIC,
|
|
CONF_NIGHTLIGHT_SWITCH,
|
|
CONF_SAVE_ON_CHANGE,
|
|
CONF_TRANSITION,
|
|
DATA_CONFIG_ENTRIES,
|
|
DATA_CUSTOM_EFFECTS,
|
|
DATA_DEVICE,
|
|
DATA_UPDATED,
|
|
DOMAIN,
|
|
YEELIGHT_FLOW_TRANSITION_SCHEMA,
|
|
YeelightEntity,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
SUPPORT_YEELIGHT = (
|
|
SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT
|
|
)
|
|
|
|
SUPPORT_YEELIGHT_WHITE_TEMP = SUPPORT_YEELIGHT | SUPPORT_COLOR_TEMP
|
|
|
|
SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR
|
|
|
|
ATTR_MINUTES = "minutes"
|
|
|
|
SERVICE_SET_MODE = "set_mode"
|
|
SERVICE_START_FLOW = "start_flow"
|
|
SERVICE_SET_COLOR_SCENE = "set_color_scene"
|
|
SERVICE_SET_HSV_SCENE = "set_hsv_scene"
|
|
SERVICE_SET_COLOR_TEMP_SCENE = "set_color_temp_scene"
|
|
SERVICE_SET_COLOR_FLOW_SCENE = "set_color_flow_scene"
|
|
SERVICE_SET_AUTO_DELAY_OFF_SCENE = "set_auto_delay_off_scene"
|
|
|
|
EFFECT_DISCO = "Disco"
|
|
EFFECT_TEMP = "Slow Temp"
|
|
EFFECT_STROBE = "Strobe epilepsy!"
|
|
EFFECT_STROBE_COLOR = "Strobe color"
|
|
EFFECT_ALARM = "Alarm"
|
|
EFFECT_POLICE = "Police"
|
|
EFFECT_POLICE2 = "Police2"
|
|
EFFECT_CHRISTMAS = "Christmas"
|
|
EFFECT_RGB = "RGB"
|
|
EFFECT_RANDOM_LOOP = "Random Loop"
|
|
EFFECT_FAST_RANDOM_LOOP = "Fast Random Loop"
|
|
EFFECT_LSD = "LSD"
|
|
EFFECT_SLOWDOWN = "Slowdown"
|
|
EFFECT_WHATSAPP = "WhatsApp"
|
|
EFFECT_FACEBOOK = "Facebook"
|
|
EFFECT_TWITTER = "Twitter"
|
|
EFFECT_STOP = "Stop"
|
|
EFFECT_HOME = "Home"
|
|
EFFECT_NIGHT_MODE = "Night Mode"
|
|
EFFECT_DATE_NIGHT = "Date Night"
|
|
EFFECT_MOVIE = "Movie"
|
|
EFFECT_SUNRISE = "Sunrise"
|
|
EFFECT_SUNSET = "Sunset"
|
|
EFFECT_ROMANCE = "Romance"
|
|
EFFECT_HAPPY_BIRTHDAY = "Happy Birthday"
|
|
EFFECT_CANDLE_FLICKER = "Candle Flicker"
|
|
|
|
YEELIGHT_TEMP_ONLY_EFFECT_LIST = [EFFECT_TEMP, EFFECT_STOP]
|
|
|
|
YEELIGHT_MONO_EFFECT_LIST = [
|
|
EFFECT_DISCO,
|
|
EFFECT_STROBE,
|
|
EFFECT_ALARM,
|
|
EFFECT_POLICE2,
|
|
EFFECT_WHATSAPP,
|
|
EFFECT_FACEBOOK,
|
|
EFFECT_TWITTER,
|
|
EFFECT_HOME,
|
|
EFFECT_CANDLE_FLICKER,
|
|
*YEELIGHT_TEMP_ONLY_EFFECT_LIST,
|
|
]
|
|
|
|
YEELIGHT_COLOR_EFFECT_LIST = [
|
|
EFFECT_STROBE_COLOR,
|
|
EFFECT_POLICE,
|
|
EFFECT_CHRISTMAS,
|
|
EFFECT_RGB,
|
|
EFFECT_RANDOM_LOOP,
|
|
EFFECT_FAST_RANDOM_LOOP,
|
|
EFFECT_LSD,
|
|
EFFECT_SLOWDOWN,
|
|
EFFECT_NIGHT_MODE,
|
|
EFFECT_DATE_NIGHT,
|
|
EFFECT_MOVIE,
|
|
EFFECT_SUNRISE,
|
|
EFFECT_SUNSET,
|
|
EFFECT_ROMANCE,
|
|
EFFECT_HAPPY_BIRTHDAY,
|
|
*YEELIGHT_MONO_EFFECT_LIST,
|
|
]
|
|
|
|
EFFECTS_MAP = {
|
|
EFFECT_DISCO: flows.disco,
|
|
EFFECT_TEMP: flows.temp,
|
|
EFFECT_STROBE: flows.strobe,
|
|
EFFECT_STROBE_COLOR: flows.strobe_color,
|
|
EFFECT_ALARM: flows.alarm,
|
|
EFFECT_POLICE: flows.police,
|
|
EFFECT_POLICE2: flows.police2,
|
|
EFFECT_CHRISTMAS: flows.christmas,
|
|
EFFECT_RGB: flows.rgb,
|
|
EFFECT_RANDOM_LOOP: flows.random_loop,
|
|
EFFECT_LSD: flows.lsd,
|
|
EFFECT_SLOWDOWN: flows.slowdown,
|
|
EFFECT_HOME: flows.home,
|
|
EFFECT_NIGHT_MODE: flows.night_mode,
|
|
EFFECT_DATE_NIGHT: flows.date_night,
|
|
EFFECT_MOVIE: flows.movie,
|
|
EFFECT_SUNRISE: flows.sunrise,
|
|
EFFECT_SUNSET: flows.sunset,
|
|
EFFECT_ROMANCE: flows.romance,
|
|
EFFECT_HAPPY_BIRTHDAY: flows.happy_birthday,
|
|
EFFECT_CANDLE_FLICKER: flows.candle_flicker,
|
|
}
|
|
|
|
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100))
|
|
|
|
SERVICE_SCHEMA_SET_MODE = {
|
|
vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])
|
|
}
|
|
|
|
SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA
|
|
|
|
SERVICE_SCHEMA_SET_COLOR_SCENE = {
|
|
vol.Required(ATTR_RGB_COLOR): vol.All(
|
|
vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple)
|
|
),
|
|
vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
|
|
}
|
|
|
|
SERVICE_SCHEMA_SET_HSV_SCENE = {
|
|
vol.Required(ATTR_HS_COLOR): vol.All(
|
|
vol.ExactSequence(
|
|
(
|
|
vol.All(vol.Coerce(float), vol.Range(min=0, max=359)),
|
|
vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
|
|
)
|
|
),
|
|
vol.Coerce(tuple),
|
|
),
|
|
vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
|
|
}
|
|
|
|
SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE = {
|
|
vol.Required(ATTR_KELVIN): vol.All(vol.Coerce(int), vol.Range(min=1700, max=6500)),
|
|
vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
|
|
}
|
|
|
|
SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_FLOW_TRANSITION_SCHEMA
|
|
|
|
SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE = {
|
|
vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)),
|
|
vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
|
|
}
|
|
|
|
|
|
@callback
|
|
def _transitions_config_parser(transitions):
|
|
"""Parse transitions config into initialized objects."""
|
|
transition_objects = []
|
|
for transition_config in transitions:
|
|
transition, params = list(transition_config.items())[0]
|
|
transition_objects.append(getattr(yeelight, transition)(*params))
|
|
|
|
return transition_objects
|
|
|
|
|
|
@callback
|
|
def _parse_custom_effects(effects_config):
|
|
effects = {}
|
|
for config in effects_config:
|
|
params = config[CONF_FLOW_PARAMS]
|
|
action = 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 _cmd(func):
|
|
"""Define a wrapper to catch exceptions from the bulb."""
|
|
|
|
def _wrap(self, *args, **kwargs):
|
|
try:
|
|
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
|
|
return func(self, *args, **kwargs)
|
|
except BulbException as ex:
|
|
_LOGGER.error("Error when calling %s: %s", func, ex)
|
|
|
|
return _wrap
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
|
|
) -> None:
|
|
"""Set up Yeelight from a config entry."""
|
|
|
|
custom_effects = _parse_custom_effects(hass.data[DOMAIN][DATA_CUSTOM_EFFECTS])
|
|
|
|
device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE]
|
|
_LOGGER.debug("Adding %s", device.name)
|
|
|
|
nl_switch_light = device.config.get(CONF_NIGHTLIGHT_SWITCH)
|
|
|
|
lights = []
|
|
|
|
device_type = device.type
|
|
|
|
def _lights_setup_helper(klass):
|
|
lights.append(klass(device, config_entry, custom_effects=custom_effects))
|
|
|
|
if device_type == BulbType.White:
|
|
_lights_setup_helper(YeelightGenericLight)
|
|
elif device_type == BulbType.Color:
|
|
if nl_switch_light and device.is_nightlight_supported:
|
|
_lights_setup_helper(YeelightColorLightWithNightlightSwitch)
|
|
_lights_setup_helper(YeelightNightLightModeWithWithoutBrightnessControl)
|
|
else:
|
|
_lights_setup_helper(YeelightColorLightWithoutNightlightSwitch)
|
|
elif device_type == BulbType.WhiteTemp:
|
|
if nl_switch_light and device.is_nightlight_supported:
|
|
_lights_setup_helper(YeelightWithNightLight)
|
|
_lights_setup_helper(YeelightNightLightMode)
|
|
else:
|
|
_lights_setup_helper(YeelightWhiteTempWithoutNightlightSwitch)
|
|
elif device_type == BulbType.WhiteTempMood:
|
|
if nl_switch_light and device.is_nightlight_supported:
|
|
_lights_setup_helper(YeelightNightLightModeWithAmbientSupport)
|
|
_lights_setup_helper(YeelightWithAmbientAndNightlight)
|
|
else:
|
|
_lights_setup_helper(YeelightWithAmbientWithoutNightlight)
|
|
_lights_setup_helper(YeelightAmbientLight)
|
|
else:
|
|
_lights_setup_helper(YeelightGenericLight)
|
|
_LOGGER.warning(
|
|
"Cannot determine device type for %s, %s. Falling back to white only",
|
|
device.host,
|
|
device.name,
|
|
)
|
|
|
|
async_add_entities(lights, True)
|
|
_async_setup_services(hass)
|
|
|
|
|
|
@callback
|
|
def _async_setup_services(hass: HomeAssistant):
|
|
"""Set up custom services."""
|
|
|
|
async def _async_start_flow(entity, service_call):
|
|
params = {**service_call.data}
|
|
params.pop(ATTR_ENTITY_ID)
|
|
params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS])
|
|
await hass.async_add_executor_job(partial(entity.start_flow, **params))
|
|
|
|
async def _async_set_color_scene(entity, service_call):
|
|
await hass.async_add_executor_job(
|
|
partial(
|
|
entity.set_scene,
|
|
SceneClass.COLOR,
|
|
*service_call.data[ATTR_RGB_COLOR],
|
|
service_call.data[ATTR_BRIGHTNESS],
|
|
)
|
|
)
|
|
|
|
async def _async_set_hsv_scene(entity, service_call):
|
|
await hass.async_add_executor_job(
|
|
partial(
|
|
entity.set_scene,
|
|
SceneClass.HSV,
|
|
*service_call.data[ATTR_HS_COLOR],
|
|
service_call.data[ATTR_BRIGHTNESS],
|
|
)
|
|
)
|
|
|
|
async def _async_set_color_temp_scene(entity, service_call):
|
|
await hass.async_add_executor_job(
|
|
partial(
|
|
entity.set_scene,
|
|
SceneClass.CT,
|
|
service_call.data[ATTR_KELVIN],
|
|
service_call.data[ATTR_BRIGHTNESS],
|
|
)
|
|
)
|
|
|
|
async def _async_set_color_flow_scene(entity, service_call):
|
|
flow = Flow(
|
|
count=service_call.data[ATTR_COUNT],
|
|
action=Flow.actions[service_call.data[ATTR_ACTION]],
|
|
transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]),
|
|
)
|
|
await hass.async_add_executor_job(
|
|
partial(
|
|
entity.set_scene,
|
|
SceneClass.CF,
|
|
flow,
|
|
)
|
|
)
|
|
|
|
async def _async_set_auto_delay_off_scene(entity, service_call):
|
|
await hass.async_add_executor_job(
|
|
partial(
|
|
entity.set_scene,
|
|
SceneClass.AUTO_DELAY_OFF,
|
|
service_call.data[ATTR_BRIGHTNESS],
|
|
service_call.data[ATTR_MINUTES],
|
|
)
|
|
)
|
|
|
|
platform = entity_platform.current_platform.get()
|
|
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_MODE,
|
|
SERVICE_SCHEMA_SET_MODE,
|
|
"set_mode",
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_START_FLOW,
|
|
SERVICE_SCHEMA_START_FLOW,
|
|
_async_start_flow,
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_COLOR_SCENE,
|
|
SERVICE_SCHEMA_SET_COLOR_SCENE,
|
|
_async_set_color_scene,
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_HSV_SCENE,
|
|
SERVICE_SCHEMA_SET_HSV_SCENE,
|
|
_async_set_hsv_scene,
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_COLOR_TEMP_SCENE,
|
|
SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE,
|
|
_async_set_color_temp_scene,
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_COLOR_FLOW_SCENE,
|
|
SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE,
|
|
_async_set_color_flow_scene,
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_AUTO_DELAY_OFF_SCENE,
|
|
SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE,
|
|
_async_set_auto_delay_off_scene,
|
|
)
|
|
|
|
|
|
class YeelightGenericLight(YeelightEntity, LightEntity):
|
|
"""Representation of a Yeelight generic light."""
|
|
|
|
def __init__(self, device, entry, custom_effects=None):
|
|
"""Initialize the Yeelight light."""
|
|
super().__init__(device, entry)
|
|
|
|
self.config = device.config
|
|
|
|
self._brightness = None
|
|
self._color_temp = None
|
|
self._hs = None
|
|
self._effect = None
|
|
|
|
model_specs = self._bulb.get_model_specs()
|
|
self._min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"])
|
|
self._max_mireds = kelvin_to_mired(model_specs["color_temp"]["min"])
|
|
|
|
self._light_type = LightType.Main
|
|
|
|
if custom_effects:
|
|
self._custom_effects = custom_effects
|
|
else:
|
|
self._custom_effects = {}
|
|
|
|
@callback
|
|
def _schedule_immediate_update(self):
|
|
self.async_schedule_update_ha_state(True)
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Handle entity which will be added."""
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
DATA_UPDATED.format(self._device.host),
|
|
self._schedule_immediate_update,
|
|
)
|
|
)
|
|
|
|
@property
|
|
def supported_features(self) -> int:
|
|
"""Flag supported features."""
|
|
return SUPPORT_YEELIGHT
|
|
|
|
@property
|
|
def effect_list(self):
|
|
"""Return the list of supported effects."""
|
|
return self._predefined_effects + self.custom_effects_names
|
|
|
|
@property
|
|
def color_temp(self) -> int:
|
|
"""Return the color temperature."""
|
|
temp_in_k = self._get_property("ct")
|
|
if temp_in_k:
|
|
self._color_temp = kelvin_to_mired(int(temp_in_k))
|
|
return self._color_temp
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the name of the device if any."""
|
|
return self.device.name
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return true if device is on."""
|
|
return self._get_property(self._power_property) == "on"
|
|
|
|
@property
|
|
def brightness(self) -> int:
|
|
"""Return the brightness of this light between 1..255."""
|
|
temp = self._get_property(self._brightness_property)
|
|
if temp:
|
|
self._brightness = temp
|
|
return round(255 * (int(self._brightness) / 100))
|
|
|
|
@property
|
|
def min_mireds(self):
|
|
"""Return minimum supported color temperature."""
|
|
return self._min_mireds
|
|
|
|
@property
|
|
def max_mireds(self):
|
|
"""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)
|
|
|
|
@property
|
|
def light_type(self):
|
|
"""Return light type."""
|
|
return self._light_type
|
|
|
|
@property
|
|
def hs_color(self) -> tuple:
|
|
"""Return the color property."""
|
|
return self._hs
|
|
|
|
@property
|
|
def effect(self):
|
|
"""Return the current effect."""
|
|
return self._effect
|
|
|
|
# F821: https://github.com/PyCQA/pyflakes/issues/373
|
|
@property
|
|
def _bulb(self) -> "Bulb": # noqa: F821
|
|
return self.device.bulb
|
|
|
|
@property
|
|
def _properties(self) -> dict:
|
|
if self._bulb is None:
|
|
return {}
|
|
return self._bulb.last_properties
|
|
|
|
def _get_property(self, prop, default=None):
|
|
return self._properties.get(prop, default)
|
|
|
|
@property
|
|
def _brightness_property(self):
|
|
return "bright"
|
|
|
|
@property
|
|
def _power_property(self):
|
|
return "power"
|
|
|
|
@property
|
|
def _turn_on_power_mode(self):
|
|
return PowerMode.LAST
|
|
|
|
@property
|
|
def _predefined_effects(self):
|
|
return YEELIGHT_MONO_EFFECT_LIST
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the device specific state attributes."""
|
|
|
|
attributes = {"flowing": self.device.is_color_flow_enabled}
|
|
if self.device.is_nightlight_supported:
|
|
attributes["night_light"] = self.device.is_nightlight_enabled
|
|
|
|
return attributes
|
|
|
|
@property
|
|
def device(self):
|
|
"""Return yeelight device."""
|
|
return self._device
|
|
|
|
def update(self):
|
|
"""Update light properties."""
|
|
self._hs = self._get_hs_from_properties()
|
|
if not self.device.is_color_flow_enabled:
|
|
self._effect = None
|
|
|
|
def _get_hs_from_properties(self):
|
|
rgb = self._get_property("rgb")
|
|
color_mode = self._get_property("color_mode")
|
|
|
|
if not rgb or not color_mode:
|
|
return None
|
|
|
|
color_mode = int(color_mode)
|
|
if color_mode == 2: # color temperature
|
|
temp_in_k = mired_to_kelvin(self.color_temp)
|
|
return color_util.color_temperature_to_hs(temp_in_k)
|
|
if color_mode == 3: # hsv
|
|
hue = int(self._get_property("hue"))
|
|
sat = int(self._get_property("sat"))
|
|
|
|
return (hue / 360 * 65536, sat / 100 * 255)
|
|
|
|
rgb = int(rgb)
|
|
blue = rgb & 0xFF
|
|
green = (rgb >> 8) & 0xFF
|
|
red = (rgb >> 16) & 0xFF
|
|
|
|
return color_util.color_RGB_to_hs(red, green, blue)
|
|
|
|
def set_music_mode(self, mode) -> None:
|
|
"""Set the music mode on or off."""
|
|
if mode:
|
|
self._bulb.start_music()
|
|
else:
|
|
self._bulb.stop_music()
|
|
|
|
@_cmd
|
|
def set_brightness(self, brightness, duration) -> None:
|
|
"""Set bulb brightness."""
|
|
if brightness:
|
|
_LOGGER.debug("Setting brightness: %s", brightness)
|
|
self._bulb.set_brightness(
|
|
brightness / 255 * 100, duration=duration, light_type=self.light_type
|
|
)
|
|
|
|
@_cmd
|
|
def set_rgb(self, rgb, duration) -> None:
|
|
"""Set bulb's color."""
|
|
if rgb and self.supported_features & SUPPORT_COLOR:
|
|
_LOGGER.debug("Setting RGB: %s", rgb)
|
|
self._bulb.set_rgb(
|
|
rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type
|
|
)
|
|
|
|
@_cmd
|
|
def set_colortemp(self, colortemp, duration) -> None:
|
|
"""Set bulb's color temperature."""
|
|
if colortemp and self.supported_features & SUPPORT_COLOR_TEMP:
|
|
temp_in_k = mired_to_kelvin(colortemp)
|
|
_LOGGER.debug("Setting color temp: %s K", temp_in_k)
|
|
|
|
self._bulb.set_color_temp(
|
|
temp_in_k, duration=duration, light_type=self.light_type
|
|
)
|
|
|
|
@_cmd
|
|
def set_default(self) -> None:
|
|
"""Set current options as default."""
|
|
self._bulb.set_default()
|
|
|
|
@_cmd
|
|
def set_flash(self, flash) -> None:
|
|
"""Activate flash."""
|
|
if flash:
|
|
if int(self._bulb.last_properties["color_mode"]) != 1:
|
|
_LOGGER.error("Flash supported currently only in RGB mode")
|
|
return
|
|
|
|
transition = int(self.config[CONF_TRANSITION])
|
|
if flash == FLASH_LONG:
|
|
count = 1
|
|
duration = transition * 5
|
|
if flash == FLASH_SHORT:
|
|
count = 1
|
|
duration = transition * 2
|
|
|
|
red, green, blue = color_util.color_hs_to_RGB(*self._hs)
|
|
|
|
transitions = []
|
|
transitions.append(
|
|
RGBTransition(255, 0, 0, brightness=10, duration=duration)
|
|
)
|
|
transitions.append(SleepTransition(duration=transition))
|
|
transitions.append(
|
|
RGBTransition(
|
|
red, green, blue, brightness=self.brightness, duration=duration
|
|
)
|
|
)
|
|
|
|
flow = Flow(count=count, transitions=transitions)
|
|
try:
|
|
self._bulb.start_flow(flow, light_type=self.light_type)
|
|
except BulbException as ex:
|
|
_LOGGER.error("Unable to set flash: %s", ex)
|
|
|
|
@_cmd
|
|
def set_effect(self, effect) -> None:
|
|
"""Activate effect."""
|
|
if not effect:
|
|
return
|
|
|
|
if effect == EFFECT_STOP:
|
|
self._bulb.stop_flow(light_type=self.light_type)
|
|
return
|
|
|
|
if effect in self.custom_effects_names:
|
|
flow = Flow(**self.custom_effects[effect])
|
|
elif effect in EFFECTS_MAP:
|
|
flow = EFFECTS_MAP[effect]()
|
|
elif effect == EFFECT_FAST_RANDOM_LOOP:
|
|
flow = flows.random_loop(duration=250)
|
|
elif effect == EFFECT_WHATSAPP:
|
|
flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102))
|
|
elif effect == EFFECT_FACEBOOK:
|
|
flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152))
|
|
elif effect == EFFECT_TWITTER:
|
|
flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237))
|
|
else:
|
|
return
|
|
|
|
try:
|
|
self._bulb.start_flow(flow, light_type=self.light_type)
|
|
self._effect = effect
|
|
except BulbException as ex:
|
|
_LOGGER.error("Unable to set effect: %s", ex)
|
|
|
|
def turn_on(self, **kwargs) -> None:
|
|
"""Turn the bulb on."""
|
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
|
colortemp = kwargs.get(ATTR_COLOR_TEMP)
|
|
hs_color = kwargs.get(ATTR_HS_COLOR)
|
|
rgb = color_util.color_hs_to_RGB(*hs_color) if hs_color else None
|
|
flash = kwargs.get(ATTR_FLASH)
|
|
effect = kwargs.get(ATTR_EFFECT)
|
|
|
|
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
|
|
|
|
self.device.turn_on(
|
|
duration=duration,
|
|
light_type=self.light_type,
|
|
power_mode=self._turn_on_power_mode,
|
|
)
|
|
|
|
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
|
|
try:
|
|
self.set_music_mode(self.config[CONF_MODE_MUSIC])
|
|
except BulbException as ex:
|
|
_LOGGER.error(
|
|
"Unable to turn on music mode, consider disabling it: %s", ex
|
|
)
|
|
|
|
try:
|
|
# values checked for none in methods
|
|
self.set_rgb(rgb, duration)
|
|
self.set_colortemp(colortemp, duration)
|
|
self.set_brightness(brightness, duration)
|
|
self.set_flash(flash)
|
|
self.set_effect(effect)
|
|
except BulbException as ex:
|
|
_LOGGER.error("Unable to set bulb properties: %s", ex)
|
|
return
|
|
|
|
# save the current state if we had a manual change.
|
|
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
|
|
try:
|
|
self.set_default()
|
|
except BulbException as ex:
|
|
_LOGGER.error("Unable to set the defaults: %s", ex)
|
|
return
|
|
self.device.update()
|
|
|
|
def turn_off(self, **kwargs) -> None:
|
|
"""Turn off."""
|
|
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
|
|
|
|
self.device.turn_off(duration=duration, light_type=self.light_type)
|
|
self.device.update()
|
|
|
|
def set_mode(self, mode: str):
|
|
"""Set a power mode."""
|
|
try:
|
|
self._bulb.set_power_mode(PowerMode[mode.upper()])
|
|
self.device.update()
|
|
except BulbException as ex:
|
|
_LOGGER.error("Unable to set the power mode: %s", ex)
|
|
|
|
def start_flow(self, transitions, count=0, action=ACTION_RECOVER):
|
|
"""Start flow."""
|
|
try:
|
|
flow = Flow(
|
|
count=count, action=Flow.actions[action], transitions=transitions
|
|
)
|
|
|
|
self._bulb.start_flow(flow, light_type=self.light_type)
|
|
self.device.update()
|
|
except BulbException as ex:
|
|
_LOGGER.error("Unable to set effect: %s", ex)
|
|
|
|
def set_scene(self, scene_class, *args):
|
|
"""
|
|
Set the light directly to the specified state.
|
|
|
|
If the light is off, it will first be turned on.
|
|
"""
|
|
try:
|
|
self._bulb.set_scene(scene_class, *args)
|
|
self.device.update()
|
|
except BulbException as ex:
|
|
_LOGGER.error("Unable to set scene: %s", ex)
|
|
|
|
|
|
class YeelightColorLightSupport:
|
|
"""Representation of a Color Yeelight light support."""
|
|
|
|
@property
|
|
def supported_features(self) -> int:
|
|
"""Flag supported features."""
|
|
return SUPPORT_YEELIGHT_RGB
|
|
|
|
@property
|
|
def _predefined_effects(self):
|
|
return YEELIGHT_COLOR_EFFECT_LIST
|
|
|
|
|
|
class YeelightWhiteTempLightSupport:
|
|
"""Representation of a Color Yeelight light."""
|
|
|
|
@property
|
|
def supported_features(self) -> int:
|
|
"""Flag supported features."""
|
|
return SUPPORT_YEELIGHT_WHITE_TEMP
|
|
|
|
@property
|
|
def _predefined_effects(self):
|
|
return YEELIGHT_TEMP_ONLY_EFFECT_LIST
|
|
|
|
|
|
class YeelightNightLightSupport:
|
|
"""Representation of a Yeelight nightlight support."""
|
|
|
|
@property
|
|
def _turn_on_power_mode(self):
|
|
return PowerMode.NORMAL
|
|
|
|
|
|
class YeelightColorLightWithoutNightlightSwitch(
|
|
YeelightColorLightSupport, YeelightGenericLight
|
|
):
|
|
"""Representation of a Color Yeelight light."""
|
|
|
|
@property
|
|
def _brightness_property(self):
|
|
return "current_brightness"
|
|
|
|
|
|
class YeelightColorLightWithNightlightSwitch(
|
|
YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight
|
|
):
|
|
"""Representation of a Yeelight with rgb support and nightlight.
|
|
|
|
It represents case when nightlight switch is set to light.
|
|
"""
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return true if device is on."""
|
|
return super().is_on and not self.device.is_nightlight_enabled
|
|
|
|
|
|
class YeelightWhiteTempWithoutNightlightSwitch(
|
|
YeelightWhiteTempLightSupport, YeelightGenericLight
|
|
):
|
|
"""White temp light, when nightlight switch is not set to light."""
|
|
|
|
@property
|
|
def _brightness_property(self):
|
|
return "current_brightness"
|
|
|
|
|
|
class YeelightWithNightLight(
|
|
YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight
|
|
):
|
|
"""Representation of a Yeelight with temp only support and nightlight.
|
|
|
|
It represents case when nightlight switch is set to light.
|
|
"""
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return true if device is on."""
|
|
return super().is_on and not self.device.is_nightlight_enabled
|
|
|
|
|
|
class YeelightNightLightMode(YeelightGenericLight):
|
|
"""Representation of a Yeelight when in nightlight mode."""
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return a unique ID."""
|
|
unique = super().unique_id
|
|
return f"{unique}-nightlight"
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the name of the device if any."""
|
|
return f"{self.device.name} nightlight"
|
|
|
|
@property
|
|
def icon(self):
|
|
"""Return the icon to use in the frontend, if any."""
|
|
return "mdi:weather-night"
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return true if device is on."""
|
|
return super().is_on and self.device.is_nightlight_enabled
|
|
|
|
@property
|
|
def _brightness_property(self):
|
|
return "nl_br"
|
|
|
|
@property
|
|
def _turn_on_power_mode(self):
|
|
return PowerMode.MOONLIGHT
|
|
|
|
@property
|
|
def _predefined_effects(self):
|
|
return YEELIGHT_TEMP_ONLY_EFFECT_LIST
|
|
|
|
|
|
class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode):
|
|
"""Representation of a Yeelight, with ambient support, when in nightlight mode."""
|
|
|
|
@property
|
|
def _power_property(self):
|
|
return "main_power"
|
|
|
|
|
|
class YeelightNightLightModeWithWithoutBrightnessControl(YeelightNightLightMode):
|
|
"""Representation of a Yeelight, when in nightlight mode.
|
|
|
|
It represents case when nightlight mode brightness control is not supported.
|
|
"""
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Flag no supported features."""
|
|
return 0
|
|
|
|
|
|
class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch):
|
|
"""Representation of a Yeelight which has ambilight support.
|
|
|
|
And nightlight switch type is none.
|
|
"""
|
|
|
|
@property
|
|
def _power_property(self):
|
|
return "main_power"
|
|
|
|
|
|
class YeelightWithAmbientAndNightlight(YeelightWithNightLight):
|
|
"""Representation of a Yeelight which has ambilight support.
|
|
|
|
And nightlight switch type is set to light.
|
|
"""
|
|
|
|
@property
|
|
def _power_property(self):
|
|
return "main_power"
|
|
|
|
|
|
class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch):
|
|
"""Representation of a Yeelight ambient light."""
|
|
|
|
PROPERTIES_MAPPING = {"color_mode": "bg_lmode"}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initialize the Yeelight Ambient light."""
|
|
super().__init__(*args, **kwargs)
|
|
self._min_mireds = kelvin_to_mired(6500)
|
|
self._max_mireds = kelvin_to_mired(1700)
|
|
|
|
self._light_type = LightType.Ambient
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return a unique ID."""
|
|
unique = super().unique_id
|
|
return f"{unique}-ambilight"
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the name of the device if any."""
|
|
return f"{self.device.name} ambilight"
|
|
|
|
@property
|
|
def _brightness_property(self):
|
|
return "bright"
|
|
|
|
def _get_property(self, prop, default=None):
|
|
bg_prop = self.PROPERTIES_MAPPING.get(prop)
|
|
|
|
if not bg_prop:
|
|
bg_prop = f"bg_{prop}"
|
|
|
|
return super()._get_property(bg_prop, default)
|