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 comment
pull/24544/head
zewelor 2019-06-13 18:42:47 +02:00 committed by Teemu R
parent 3d03a86b13
commit 6d3c3ce449
2 changed files with 185 additions and 128 deletions

View File

@ -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()

View File

@ -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)