* yeelight: adjust supported features on update() Earlier we checked for the type only during the initialization, but this won't work when the bulb is disconnected during the init, causing failures to adjust rgb&color temperature even if those should be supported. fixes #6692 * Use reassign instead of OR for updating the supported features
314 lines
10 KiB
Python
314 lines
10 KiB
Python
"""
|
|
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/
|
|
"""
|
|
import logging
|
|
import colorsys
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.util.color import (
|
|
color_temperature_mired_to_kelvin as mired_to_kelvin,
|
|
color_temperature_kelvin_to_mired as kelvin_to_mired,
|
|
color_temperature_to_rgb)
|
|
from homeassistant.const import CONF_DEVICES, CONF_NAME
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
|
|
ATTR_FLASH, FLASH_SHORT, FLASH_LONG,
|
|
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
|
|
SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
|
|
Light, PLATFORM_SCHEMA)
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
REQUIREMENTS = ['yeelight==0.2.2']
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_TRANSITION = "transition"
|
|
DEFAULT_TRANSITION = 350
|
|
|
|
CONF_SAVE_ON_CHANGE = "save_on_change"
|
|
CONF_MODE_MUSIC = "use_music_mode"
|
|
|
|
DOMAIN = 'yeelight'
|
|
|
|
DEVICE_SCHEMA = vol.Schema({
|
|
vol.Optional(CONF_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=True): cv.boolean,
|
|
})
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
{vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, })
|
|
|
|
SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
|
|
SUPPORT_TRANSITION |
|
|
SUPPORT_FLASH)
|
|
|
|
SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT |
|
|
SUPPORT_RGB_COLOR |
|
|
SUPPORT_COLOR_TEMP)
|
|
|
|
|
|
def _cmd(func):
|
|
"""A wrapper to catch exceptions from the bulb."""
|
|
def _wrap(self, *args, **kwargs):
|
|
import yeelight
|
|
try:
|
|
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
|
|
return func(self, *args, **kwargs)
|
|
except yeelight.BulbException as ex:
|
|
_LOGGER.error("Error when calling %s: %s", func, ex)
|
|
|
|
return _wrap
|
|
|
|
|
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
"""Setup the Yeelight bulbs."""
|
|
lights = []
|
|
if discovery_info is not None:
|
|
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
|
|
|
|
# not using hostname, as it seems to vary.
|
|
name = "yeelight_%s_%s" % (discovery_info["device_type"],
|
|
discovery_info["properties"]["mac"])
|
|
device = {'name': name, 'ipaddr': discovery_info['host']}
|
|
|
|
lights.append(YeelightLight(device, DEVICE_SCHEMA({})))
|
|
else:
|
|
for ipaddr, device_config in config[CONF_DEVICES].items():
|
|
_LOGGER.debug("Adding configured %s", device_config[CONF_NAME])
|
|
|
|
device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr}
|
|
lights.append(YeelightLight(device, device_config))
|
|
|
|
add_devices(lights, True) # true to request an update before adding.
|
|
|
|
|
|
class YeelightLight(Light):
|
|
"""Representation of a Yeelight light."""
|
|
|
|
def __init__(self, device, config):
|
|
"""Initialize the light."""
|
|
self.config = config
|
|
self._name = device['name']
|
|
self._ipaddr = device['ipaddr']
|
|
|
|
self._supported_features = SUPPORT_YEELIGHT
|
|
self._available = False
|
|
self._bulb_device = None
|
|
|
|
self._brightness = None
|
|
self._color_temp = None
|
|
self._is_on = None
|
|
self._rgb = None
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return if bulb is available."""
|
|
return self._available
|
|
|
|
@property
|
|
def supported_features(self) -> int:
|
|
"""Flag supported features."""
|
|
return self._supported_features
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return the ID of this light."""
|
|
return "{}.{}".format(self.__class__, self._ipaddr)
|
|
|
|
@property
|
|
def color_temp(self) -> int:
|
|
"""Return the color temperature."""
|
|
return self._color_temp
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the name of the device if any."""
|
|
return self._name
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return true if device is on."""
|
|
return self._is_on
|
|
|
|
@property
|
|
def brightness(self) -> int:
|
|
"""Return the brightness of this light between 1..255."""
|
|
return self._brightness
|
|
|
|
def _get_rgb_from_properties(self):
|
|
rgb = self._properties.get("rgb", None)
|
|
color_mode = self._properties.get("color_mode", None)
|
|
if not rgb or not color_mode:
|
|
return rgb
|
|
|
|
color_mode = int(color_mode)
|
|
if color_mode == 2: # color temperature
|
|
return color_temperature_to_rgb(self.color_temp)
|
|
if color_mode == 3: # hsv
|
|
hue = self._properties.get("hue")
|
|
sat = self._properties.get("sat")
|
|
val = self._properties.get("bright")
|
|
return colorsys.hsv_to_rgb(hue, sat, val)
|
|
|
|
rgb = int(rgb)
|
|
blue = rgb & 0xff
|
|
green = (rgb >> 8) & 0xff
|
|
red = (rgb >> 16) & 0xff
|
|
|
|
return red, green, blue
|
|
|
|
@property
|
|
def rgb_color(self) -> tuple:
|
|
"""Return the color property."""
|
|
return self._rgb
|
|
|
|
@property
|
|
def _properties(self) -> dict:
|
|
return self._bulb.last_properties
|
|
|
|
@property
|
|
def _bulb(self) -> object:
|
|
import yeelight
|
|
if self._bulb_device is None:
|
|
try:
|
|
self._bulb_device = yeelight.Bulb(self._ipaddr)
|
|
self._bulb_device.get_properties() # force init for type
|
|
|
|
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)
|
|
|
|
return self._bulb_device
|
|
|
|
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()
|
|
|
|
def update(self) -> None:
|
|
"""Update properties from the bulb."""
|
|
import yeelight
|
|
try:
|
|
self._bulb.get_properties()
|
|
|
|
if self._bulb_device.bulb_type == yeelight.BulbType.Color:
|
|
self._supported_features = SUPPORT_YEELIGHT_RGB
|
|
|
|
self._is_on = self._properties.get("power") == "on"
|
|
|
|
bright = self._properties.get("bright", None)
|
|
if bright:
|
|
self._brightness = 255 * (int(bright) / 100)
|
|
|
|
temp_in_k = self._properties.get("ct", None)
|
|
if temp_in_k:
|
|
self._color_temp = kelvin_to_mired(int(temp_in_k))
|
|
|
|
self._rgb = self._get_rgb_from_properties()
|
|
|
|
self._available = True
|
|
except yeelight.BulbException as ex:
|
|
if self._available: # just inform once
|
|
_LOGGER.error("Unable to update bulb status: %s", ex)
|
|
self._available = False
|
|
|
|
@_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)
|
|
|
|
@_cmd
|
|
def set_rgb(self, rgb, duration) -> None:
|
|
"""Set bulb's color."""
|
|
if rgb and self.supported_features & SUPPORT_RGB_COLOR:
|
|
_LOGGER.debug("Setting RGB: %s", rgb)
|
|
self._bulb.set_rgb(rgb[0], rgb[1], rgb[2], duration=duration)
|
|
|
|
@_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)
|
|
|
|
@_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:
|
|
from yeelight import RGBTransition, SleepTransition, Flow
|
|
if 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 = self.rgb_color
|
|
|
|
transitions = list()
|
|
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)
|
|
self._bulb.start_flow(flow)
|
|
|
|
def turn_on(self, **kwargs) -> None:
|
|
"""Turn the bulb on."""
|
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
|
colortemp = kwargs.get(ATTR_COLOR_TEMP)
|
|
rgb = kwargs.get(ATTR_RGB_COLOR)
|
|
flash = kwargs.get(ATTR_FLASH)
|
|
|
|
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._bulb.turn_on(duration=duration)
|
|
|
|
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
|
|
self.set_music_mode(self.config[CONF_MODE_MUSIC])
|
|
|
|
# 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)
|
|
|
|
# save the current state if we had a manual change.
|
|
if self.config[CONF_SAVE_ON_CHANGE]:
|
|
if brightness or colortemp or rgb:
|
|
self.set_default()
|
|
|
|
def turn_off(self, **kwargs) -> None:
|
|
"""Turn off."""
|
|
self._bulb.turn_off()
|