Refactor yeelight code (#22547)
* Separate yeelight light classes * Removed not used variable * Allow to create device right away, when model is declared * Lint fixes * Use correct brightness, when nightlight mode is on * Pylint fix * Add power property * Fix imports * Update homeassistant/components/yeelight/light.py Co-Authored-By: Teemu R. <tpr@iki.fi> * Small PR fixes * Simplify device to yeelight class mapping * Simplify device initialization code * Fix commentpull/24544/head
parent
3d03a86b13
commit
6d3c3ce449
|
@ -14,7 +14,8 @@ from homeassistant.components.binary_sensor import DOMAIN as \
|
|||
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.dispatcher import dispatcher_send, \
|
||||
dispatcher_connect
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -22,6 +23,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DOMAIN = "yeelight"
|
||||
DATA_YEELIGHT = DOMAIN
|
||||
DATA_UPDATED = 'yeelight_{}_data_updated'
|
||||
DEVICE_INITIALIZED = '{}_device_initialized'.format(DOMAIN)
|
||||
|
||||
DEFAULT_NAME = 'Yeelight'
|
||||
DEFAULT_TRANSITION = 350
|
||||
|
@ -41,6 +43,8 @@ ACTION_RECOVER = 'recover'
|
|||
ACTION_STAY = 'stay'
|
||||
ACTION_OFF = 'off'
|
||||
|
||||
ACTIVE_MODE_NIGHTLIGHT = '1'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
YEELIGHT_RGB_TRANSITION = 'RGBTransition'
|
||||
|
@ -115,7 +119,7 @@ def setup(hass, config):
|
|||
conf = config.get(DOMAIN, {})
|
||||
yeelight_data = hass.data[DATA_YEELIGHT] = {}
|
||||
|
||||
def device_discovered(service, info):
|
||||
def device_discovered(_, info):
|
||||
_LOGGER.debug("Adding autodetected %s", info['hostname'])
|
||||
|
||||
device_type = info['device_type']
|
||||
|
@ -132,7 +136,7 @@ def setup(hass, config):
|
|||
|
||||
discovery.listen(hass, SERVICE_YEELIGHT, device_discovered)
|
||||
|
||||
def update(event):
|
||||
def update(_):
|
||||
for device in list(yeelight_data.values()):
|
||||
device.update()
|
||||
|
||||
|
@ -140,6 +144,17 @@ def setup(hass, config):
|
|||
hass, update, conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
)
|
||||
|
||||
def load_platforms(ipaddr):
|
||||
platform_config = hass.data[DATA_YEELIGHT][ipaddr].config.copy()
|
||||
platform_config[CONF_HOST] = ipaddr
|
||||
platform_config[CONF_CUSTOM_EFFECTS] = \
|
||||
config.get(DOMAIN, {}).get(CONF_CUSTOM_EFFECTS, {})
|
||||
load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, config)
|
||||
load_platform(hass, BINARY_SENSOR_DOMAIN, DOMAIN, platform_config,
|
||||
config)
|
||||
|
||||
dispatcher_connect(hass, DEVICE_INITIALIZED, load_platforms)
|
||||
|
||||
if DOMAIN in config:
|
||||
for ipaddr, device_config in conf[CONF_DEVICES].items():
|
||||
_LOGGER.debug("Adding configured %s", device_config[CONF_NAME])
|
||||
|
@ -148,7 +163,7 @@ def setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
def _setup_device(hass, hass_config, ipaddr, device_config):
|
||||
def _setup_device(hass, _, ipaddr, device_config):
|
||||
devices = hass.data[DATA_YEELIGHT]
|
||||
|
||||
if ipaddr in devices:
|
||||
|
@ -157,15 +172,7 @@ def _setup_device(hass, hass_config, ipaddr, device_config):
|
|||
device = YeelightDevice(hass, ipaddr, device_config)
|
||||
|
||||
devices[ipaddr] = device
|
||||
|
||||
platform_config = device_config.copy()
|
||||
platform_config[CONF_HOST] = ipaddr
|
||||
platform_config[CONF_CUSTOM_EFFECTS] = \
|
||||
hass_config.get(DOMAIN, {}).get(CONF_CUSTOM_EFFECTS, {})
|
||||
|
||||
load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, hass_config)
|
||||
load_platform(hass, BINARY_SENSOR_DOMAIN, DOMAIN, platform_config,
|
||||
hass_config)
|
||||
hass.add_job(device.setup)
|
||||
|
||||
|
||||
class YeelightDevice:
|
||||
|
@ -178,24 +185,14 @@ class YeelightDevice:
|
|||
self._ipaddr = ipaddr
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._model = config.get(CONF_MODEL)
|
||||
self._bulb_device = None
|
||||
self._bulb_device = Bulb(self.ipaddr, model=self._model)
|
||||
self._device_type = None
|
||||
self._available = False
|
||||
self._initialized = False
|
||||
|
||||
@property
|
||||
def bulb(self):
|
||||
"""Return bulb device."""
|
||||
if self._bulb_device is None:
|
||||
try:
|
||||
self._bulb_device = Bulb(self._ipaddr, model=self._model)
|
||||
# force init for type
|
||||
self.update()
|
||||
|
||||
self._available = True
|
||||
except 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
|
||||
|
||||
@property
|
||||
|
@ -218,23 +215,38 @@ class YeelightDevice:
|
|||
"""Return true is device is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return configured device model."""
|
||||
return self._model
|
||||
|
||||
@property
|
||||
def is_nightlight_enabled(self) -> bool:
|
||||
"""Return true / false if nightlight is currently enabled."""
|
||||
if self.bulb is None:
|
||||
return False
|
||||
|
||||
return self.bulb.last_properties.get('active_mode') == '1'
|
||||
return self._active_mode == ACTIVE_MODE_NIGHTLIGHT
|
||||
|
||||
@property
|
||||
def is_nightlight_supported(self) -> bool:
|
||||
"""Return true / false if nightlight is supported."""
|
||||
return self.bulb.get_model_specs().get('night_light', False)
|
||||
if self.model:
|
||||
return self.bulb.get_model_specs().get('night_light', False)
|
||||
|
||||
return self._active_mode is not None
|
||||
|
||||
@property
|
||||
def is_ambilight_supported(self) -> bool:
|
||||
"""Return true / false if ambilight is supported."""
|
||||
return self.bulb.get_model_specs().get('background_light', False)
|
||||
def _active_mode(self):
|
||||
return self.bulb.last_properties.get('active_mode')
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Return bulb type."""
|
||||
if not self._device_type:
|
||||
self._device_type = self.bulb.bulb_type
|
||||
|
||||
return self._device_type
|
||||
|
||||
def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None):
|
||||
"""Turn on device."""
|
||||
|
@ -242,17 +254,16 @@ class YeelightDevice:
|
|||
self.bulb.turn_on(duration=duration, light_type=light_type)
|
||||
except BulbException as ex:
|
||||
_LOGGER.error("Unable to turn the bulb on: %s", ex)
|
||||
return
|
||||
|
||||
def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None):
|
||||
"""Turn off device."""
|
||||
try:
|
||||
self.bulb.turn_off(duration=duration, light_type=light_type)
|
||||
except BulbException as ex:
|
||||
_LOGGER.error("Unable to turn the bulb off: %s", ex)
|
||||
return
|
||||
_LOGGER.error("Unable to turn the bulb off: %s, %s: %s",
|
||||
self.ipaddr, self.name, ex)
|
||||
|
||||
def update(self):
|
||||
def _update_properties(self):
|
||||
"""Read new properties from the device."""
|
||||
if not self.bulb:
|
||||
return
|
||||
|
@ -260,9 +271,29 @@ class YeelightDevice:
|
|||
try:
|
||||
self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES)
|
||||
self._available = True
|
||||
if not self._initialized:
|
||||
self._initialize_device()
|
||||
except BulbException as ex:
|
||||
if self._available: # just inform once
|
||||
_LOGGER.error("Unable to update bulb status: %s", ex)
|
||||
_LOGGER.error("Unable to update device %s, %s: %s",
|
||||
self.ipaddr, self.name, ex)
|
||||
self._available = False
|
||||
|
||||
return self._available
|
||||
|
||||
def _initialize_device(self):
|
||||
self._initialized = True
|
||||
dispatcher_send(self._hass, DEVICE_INITIALIZED, self.ipaddr)
|
||||
|
||||
def update(self):
|
||||
"""Update device properties and send data updated signal."""
|
||||
self._update_properties()
|
||||
dispatcher_send(self._hass, DATA_UPDATED.format(self._ipaddr))
|
||||
|
||||
def setup(self):
|
||||
"""Fetch initial device properties."""
|
||||
initial_update = self._update_properties()
|
||||
|
||||
# We can build correct class anyway.
|
||||
if not initial_update and self.model:
|
||||
self._initialize_device()
|
||||
|
|
|
@ -80,6 +80,19 @@ YEELIGHT_EFFECT_LIST = [
|
|||
EFFECT_TWITTER,
|
||||
EFFECT_STOP]
|
||||
|
||||
MODEL_TO_DEVICE_TYPE = {
|
||||
'mono': BulbType.White,
|
||||
'mono1': BulbType.White,
|
||||
'color': BulbType.Color,
|
||||
'color1': BulbType.Color,
|
||||
'color2': BulbType.Color,
|
||||
'strip1': BulbType.Color,
|
||||
'bslamp1': BulbType.Color,
|
||||
'ceiling1': BulbType.WhiteTemp,
|
||||
'ceiling2': BulbType.WhiteTemp,
|
||||
'ceiling3': BulbType.WhiteTemp,
|
||||
'ceiling4': BulbType.WhiteTempMood}
|
||||
|
||||
|
||||
def _transitions_config_parser(transitions):
|
||||
"""Parse transitions config into initialized objects."""
|
||||
|
@ -137,11 +150,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS])
|
||||
|
||||
lights = [YeelightLight(device, custom_effects=custom_effects)]
|
||||
lights = []
|
||||
|
||||
if device.is_ambilight_supported:
|
||||
lights.append(
|
||||
YeelightAmbientLight(device, custom_effects=custom_effects))
|
||||
if device.model:
|
||||
device_type = MODEL_TO_DEVICE_TYPE.get(device.model, None)
|
||||
else:
|
||||
device_type = device.type
|
||||
|
||||
def _lights_setup_helper(klass):
|
||||
lights.append(klass(device, custom_effects=custom_effects))
|
||||
|
||||
if device_type == BulbType.White:
|
||||
_lights_setup_helper(YeelightGenericLight)
|
||||
elif device_type == BulbType.Color:
|
||||
_lights_setup_helper(YeelightColorLight)
|
||||
elif device_type == BulbType.WhiteTemp:
|
||||
_lights_setup_helper(YeelightWhiteTempLight)
|
||||
elif device_type == BulbType.WhiteTempMood:
|
||||
_lights_setup_helper(YeelightWithAmbientLight)
|
||||
_lights_setup_helper(YeelightAmbientLight)
|
||||
else:
|
||||
_LOGGER.error("Cannot determine device type for %s, %s",
|
||||
device.ipaddr, device.name)
|
||||
|
||||
hass.data[data_key] += lights
|
||||
add_entities(lights, True)
|
||||
|
@ -179,23 +209,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
schema=service_schema_start_flow)
|
||||
|
||||
|
||||
class YeelightLight(Light):
|
||||
"""Representation of a Yeelight light."""
|
||||
class YeelightGenericLight(Light):
|
||||
"""Representation of a Yeelight generic light."""
|
||||
|
||||
def __init__(self, device, custom_effects=None):
|
||||
"""Initialize the Yeelight light."""
|
||||
self.config = device.config
|
||||
self._device = device
|
||||
|
||||
self._supported_features = SUPPORT_YEELIGHT
|
||||
|
||||
self._brightness = None
|
||||
self._color_temp = None
|
||||
self._is_on = None
|
||||
self._hs = None
|
||||
|
||||
self._min_mireds = None
|
||||
self._max_mireds = 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
|
||||
|
||||
|
@ -229,7 +257,7 @@ class YeelightLight(Light):
|
|||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
return SUPPORT_YEELIGHT
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
|
@ -239,7 +267,10 @@ class YeelightLight(Light):
|
|||
@property
|
||||
def color_temp(self) -> int:
|
||||
"""Return the color temperature."""
|
||||
return self._color_temp
|
||||
temp = self._get_property('ct')
|
||||
if temp:
|
||||
self._color_temp = temp
|
||||
return kelvin_to_mired(int(self._color_temp))
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -249,12 +280,15 @@ class YeelightLight(Light):
|
|||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on."""
|
||||
return self._is_on
|
||||
return self._get_property(self._power_property) == 'on'
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of this light between 1..255."""
|
||||
return self._brightness
|
||||
temp = self._get_property(self._brightness_property)
|
||||
if temp:
|
||||
self._brightness = temp
|
||||
return round(255 * (int(self._brightness) / 100))
|
||||
|
||||
@property
|
||||
def min_mireds(self):
|
||||
|
@ -281,6 +315,42 @@ class YeelightLight(Light):
|
|||
"""Return light type."""
|
||||
return self._light_type
|
||||
|
||||
@property
|
||||
def hs_color(self) -> tuple:
|
||||
"""Return the color property."""
|
||||
return self._hs
|
||||
|
||||
# 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 device(self):
|
||||
"""Return yeelight device."""
|
||||
return self._device
|
||||
|
||||
def update(self):
|
||||
"""Update light properties."""
|
||||
self._hs = self._get_hs_from_properties()
|
||||
|
||||
def _get_hs_from_properties(self):
|
||||
rgb = self._get_property('rgb')
|
||||
color_mode = self._get_property('color_mode')
|
||||
|
@ -290,7 +360,7 @@ class YeelightLight(Light):
|
|||
|
||||
color_mode = int(color_mode)
|
||||
if color_mode == 2: # color temperature
|
||||
temp_in_k = mired_to_kelvin(self._color_temp)
|
||||
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'))
|
||||
|
@ -305,34 +375,6 @@ class YeelightLight(Light):
|
|||
|
||||
return color_util.color_RGB_to_hs(red, green, blue)
|
||||
|
||||
@property
|
||||
def hs_color(self) -> tuple:
|
||||
"""Return the color property."""
|
||||
return self._hs
|
||||
|
||||
@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 device(self):
|
||||
"""Return yeelight device."""
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def _is_nightlight_enabled(self):
|
||||
return self.device.is_nightlight_enabled
|
||||
|
||||
# F821: https://github.com/PyCQA/pyflakes/issues/373
|
||||
@property
|
||||
def _bulb(self) -> 'yeelight.Bulb': # noqa: F821
|
||||
return self.device.bulb
|
||||
|
||||
def set_music_mode(self, mode) -> None:
|
||||
"""Set the music mode on or off."""
|
||||
if mode:
|
||||
|
@ -340,47 +382,6 @@ class YeelightLight(Light):
|
|||
else:
|
||||
self._bulb.stop_music()
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update properties from the bulb."""
|
||||
bulb_type = self._bulb.bulb_type
|
||||
|
||||
if bulb_type == BulbType.Color:
|
||||
self._supported_features = SUPPORT_YEELIGHT_RGB
|
||||
elif self.light_type == LightType.Ambient:
|
||||
self._supported_features = SUPPORT_YEELIGHT_RGB
|
||||
elif bulb_type in (BulbType.WhiteTemp, BulbType.WhiteTempMood):
|
||||
if self._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()
|
||||
self._min_mireds = \
|
||||
kelvin_to_mired(model_specs['color_temp']['max'])
|
||||
self._max_mireds = \
|
||||
kelvin_to_mired(model_specs['color_temp']['min'])
|
||||
|
||||
if bulb_type == BulbType.WhiteTempMood:
|
||||
self._is_on = self._get_property('main_power') == 'on'
|
||||
else:
|
||||
self._is_on = self._get_property('power') == 'on'
|
||||
|
||||
if self._is_nightlight_enabled:
|
||||
bright = self._get_property('nl_br')
|
||||
else:
|
||||
bright = self._get_property('bright')
|
||||
|
||||
if bright:
|
||||
self._brightness = round(255 * (int(bright) / 100))
|
||||
|
||||
temp_in_k = self._get_property('ct')
|
||||
|
||||
if temp_in_k:
|
||||
self._color_temp = kelvin_to_mired(int(temp_in_k))
|
||||
|
||||
self._hs = self._get_hs_from_properties()
|
||||
|
||||
@_cmd
|
||||
def set_brightness(self, brightness, duration) -> None:
|
||||
"""Set bulb brightness."""
|
||||
|
@ -566,12 +567,41 @@ class YeelightLight(Light):
|
|||
_LOGGER.error("Unable to set effect: %s", ex)
|
||||
|
||||
|
||||
class YeelightAmbientLight(YeelightLight):
|
||||
class YeelightColorLight(YeelightGenericLight):
|
||||
"""Representation of a Color Yeelight light."""
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_YEELIGHT_RGB
|
||||
|
||||
|
||||
class YeelightWhiteTempLight(YeelightGenericLight):
|
||||
"""Representation of a Color Yeelight light."""
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_YEELIGHT_WHITE_TEMP
|
||||
|
||||
@property
|
||||
def _brightness_property(self):
|
||||
return 'current_brightness'
|
||||
|
||||
|
||||
class YeelightWithAmbientLight(YeelightWhiteTempLight):
|
||||
"""Representation of a Yeelight which has ambilight support."""
|
||||
|
||||
@ property
|
||||
def _power_property(self):
|
||||
return 'main_power'
|
||||
|
||||
|
||||
class YeelightAmbientLight(YeelightColorLight):
|
||||
"""Representation of a Yeelight ambient light."""
|
||||
|
||||
PROPERTIES_MAPPING = {
|
||||
"color_mode": "bg_lmode",
|
||||
"main_power": "bg_power",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -587,14 +617,10 @@ class YeelightAmbientLight(YeelightLight):
|
|||
"""Return the name of the device if any."""
|
||||
return "{} ambilight".format(self.device.name)
|
||||
|
||||
@property
|
||||
def _is_nightlight_enabled(self):
|
||||
return False
|
||||
|
||||
def _get_property(self, prop, default=None):
|
||||
bg_prop = self.PROPERTIES_MAPPING.get(prop)
|
||||
|
||||
if not bg_prop:
|
||||
bg_prop = "bg_" + prop
|
||||
|
||||
return self._properties.get(bg_prop, default)
|
||||
return super()._get_property(bg_prop, default)
|
||||
|
|
Loading…
Reference in New Issue