Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Yeelight support (expose more properties, add support for secondary lights) #1035

Merged
merged 15 commits into from
May 16, 2021
Merged
43 changes: 29 additions & 14 deletions miio/tests/test_yeelight.py
Expand Up @@ -21,6 +21,21 @@ def __init__(self, *args, **kwargs):
"name": "test name",
"lan_ctrl": "1",
"save_state": "1",
"delayoff": "60",
"music_on": "1",
"flowing": "0",
"flow_params": "",
"active_mode": "1",
"nl_br": "100",
"bg_power": "off",
"bg_bright": "100",
"bg_lmode": "2",
"bg_rgb": "16711680",
"bg_hue": "359",
"bg_sat": "100",
"bg_ct": "3584",
"bg_flowing": "0",
"bg_flow_params": "",
}

self.return_values = {
Expand Down Expand Up @@ -66,11 +81,11 @@ def test_status(self):
assert repr(status) == repr(YeelightStatus(self.device.start_state))

assert status.name == self.device.start_state["name"]
assert status.is_on is False
assert status.brightness == 100
assert status.color_temp == 3584
assert status.color_mode == YeelightMode.ColorTemperature
assert status.rgb is None
assert status.lights[0].is_on is False
assert status.lights[0].brightness == 100
assert status.lights[0].color_temp == 3584
assert status.lights[0].color_mode == YeelightMode.ColorTemperature
assert status.lights[0].rgb is None
assert status.developer_mode is True
rytilahti marked this conversation as resolved.
Show resolved Hide resolved
assert status.save_state_on_change is True

Expand All @@ -80,21 +95,21 @@ def test_status(self):

def test_on(self):
self.device.off() # make sure we are off
assert self.device.status().is_on is False
assert self.device.status().lights[0].is_on is False

self.device.on()
assert self.device.status().is_on is True
assert self.device.status().lights[0].is_on is True

def test_off(self):
self.device.on() # make sure we are on
assert self.device.status().is_on is True
assert self.device.status().lights[0].is_on is True

self.device.off()
assert self.device.status().is_on is False
assert self.device.status().lights[0].is_on is False

def test_set_brightness(self):
def brightness():
return self.device.status().brightness
return self.device.status().lights[0].brightness

self.device.set_brightness(50)
assert brightness() == 50
Expand All @@ -110,7 +125,7 @@ def brightness():

def test_set_color_temp(self):
def color_temp():
return self.device.status().color_temp
return self.device.status().lights[0].color_temp

self.device.set_color_temp(2000)
assert color_temp() == 2000
Expand All @@ -125,7 +140,7 @@ def color_temp():

def test_set_rgb(self):
def rgb():
return self.device.status().rgb
return self.device.status().lights[0].rgb

self.device._reset_state()
self.device._set_state("color_mode", [1])
Expand Down Expand Up @@ -160,7 +175,7 @@ def rgb():
@pytest.mark.skip("hsv is not properly implemented")
def test_set_hsv(self):
self.reset_state()
hue, sat, val = self.device.status().hsv
hue, sat, val = self.device.status().lights[0].hsv
assert hue == 359
assert sat == 100
assert val == 100
Expand Down Expand Up @@ -200,7 +215,7 @@ def name():

def test_toggle(self):
def is_on():
return self.device.status().is_on
return self.device.status().lights[0].is_on

orig_state = is_on()
self.device.toggle()
Expand Down
172 changes: 146 additions & 26 deletions miio/yeelight.py
@@ -1,6 +1,6 @@
import warnings
from enum import IntEnum
from typing import Optional, Tuple
from typing import List, Optional, Tuple

import click

Expand All @@ -14,61 +14,120 @@ class YeelightException(DeviceException):
pass


class YeelightSubLightType(IntEnum):
Main = 1
Background = 2


SUBLIGHT_PROP_PREFIX = {
YeelightSubLightType.Main: "",
YeelightSubLightType.Background: "bg_",
}

SUBLIGHT_COLOR_MODE_PROP = {
YeelightSubLightType.Main: "color_mode",
YeelightSubLightType.Background: "bg_lmode",
}


class YeelightMode(IntEnum):
RGB = 1
ColorTemperature = 2
HSV = 3


class YeelightStatus(DeviceStatus):
def __init__(self, data):
# ['power', 'bright', 'ct', 'rgb', 'hue', 'sat', 'color_mode', 'name', 'lan_ctrl', 'save_state']
# ['on', '100', '3584', '16711680', '359', '100', '2', 'name', '1', '1']
class YeelightSubLight:
def __init__(self, data, type):
self.data = data
self.type = type

def __repr__(self):
s = "\n"
s += f" Sub Light - {self.type.name} \n"
s += f" Power: {self.is_on}\n"
s += f" Brightness: {self.brightness}\n"
s += f" Color mode: {self.color_mode}\n"
s += f" RGB: {self.rgb}\n"
s += f" HSV: {self.hsv}\n"
s += f" Temperature: {self.color_temp}\n"
s += f" Color flowing mode: {self.color_flowing}\n"
s += f" Color flowing parameters: {self.color_flow_params}\n"
return s

def get_prop_name(self, prop) -> str:
if prop == "color_mode":
return SUBLIGHT_COLOR_MODE_PROP[self.type]
else:
return SUBLIGHT_PROP_PREFIX[self.type] + prop

@property
def is_on(self) -> bool:
"""Return whether the bulb is on or off."""
return self.data["power"] == "on"
"""Return whether the light is on or off."""
return self.data[self.get_prop_name("power")] == "on"

@property
def brightness(self) -> int:
"""Return current brightness."""
return int(self.data["bright"])
return int(self.data[self.get_prop_name("bright")])

@property
def rgb(self) -> Optional[Tuple[int, int, int]]:
"""Return color in RGB if RGB mode is active."""
rgb = self.data["rgb"]
rgb = self.data[self.get_prop_name("rgb")]
if self.color_mode == YeelightMode.RGB and rgb:
return int_to_rgb(int(rgb))
return None

@property
def color_mode(self) -> YeelightMode:
"""Return current color mode."""
return YeelightMode(int(self.data["color_mode"]))
return YeelightMode(int(self.data[self.get_prop_name("color_mode")]))

@property
def hsv(self) -> Optional[Tuple[int, int, int]]:
"""Return current color in HSV if HSV mode is active."""
hue = self.data["hue"]
sat = self.data["sat"]
brightness = self.data["bright"]
hue = self.data[self.get_prop_name("hue")]
sat = self.data[self.get_prop_name("sat")]
brightness = self.data[self.get_prop_name("bright")]
if self.color_mode == YeelightMode.HSV and (hue or sat or brightness):
return hue, sat, brightness
return None

@property
def color_temp(self) -> Optional[int]:
"""Return current color temperature, if applicable."""
ct = self.data["ct"]
ct = self.data[self.get_prop_name("ct")]
if self.color_mode == YeelightMode.ColorTemperature and ct:
return int(ct)
return None

@property
def developer_mode(self) -> bool:
def color_flowing(self) -> bool:
"""Return whether the color flowing is active."""
return bool(int(self.data[self.get_prop_name("flowing")]))

@property
def color_flow_params(self) -> Optional[str]:
"""Return color flowing params."""
if self.color_flowing:
return self.data[self.get_prop_name("flow_params")]
return None


class YeelightStatus(DeviceStatus):
def __init__(self, data):
# yeelink.light.ceiling4, yeelink.light.ceiling20
# {'name': '', 'lan_ctrl': '1', 'save_state': '1', 'delayoff': '0', 'music_on': '', 'power': 'off', 'bright': '1', 'color_mode': '2', 'rgb': '', 'hue': '', 'sat': '', 'ct': '4115', 'flowing': '0', 'flow_params': '0,0,2000,3,0,33,2000,3,0,100', 'active_mode': '1', 'nl_br': '1', 'bg_power': 'off', 'bg_bright': '100', 'bg_lmode': '1', 'bg_rgb': '15531811', 'bg_hue': '65', 'bg_sat': '86', 'bg_ct': '4000', 'bg_flowing': '0', 'bg_flow_params': '0,0,3000,4,16711680,100,3000,4,65280,100,3000,4,255,100'}
# yeelink.light.ceiling1
# {'name': '', 'lan_ctrl': '1', 'save_state': '1', 'delayoff': '0', 'music_on': '', 'power': 'off', 'bright': '100', 'color_mode': '2', 'rgb': '', 'hue': '', 'sat': '', 'ct': '5200', 'flowing': '0', 'flow_params': '', 'active_mode': '0', 'nl_br': '0', 'bg_power': '', 'bg_bright': '', 'bg_lmode': '', 'bg_rgb': '', 'bg_hue': '', 'bg_sat': '', 'bg_ct': '', 'bg_flowing': '', 'bg_flow_params': ''}
# yeelink.light.ceiling22 - like yeelink.light.ceiling1 but without "lan_ctrl"
# {'name': '', 'lan_ctrl': '', 'save_state': '1', 'delayoff': '0', 'music_on': '', 'power': 'off', 'bright': '84', 'color_mode': '2', 'rgb': '', 'hue': '', 'sat': '', 'ct': '4000', 'flowing': '0', 'flow_params': '0,0,800,2,2700,50,800,2,2700,30,1200,2,2700,80,800,2,2700,60,1200,2,2700,90,2400,2,2700,50,1200,2,2700,80,800,2,2700,60,400,2,2700,70', 'active_mode': '0', 'nl_br': '0', 'bg_power': '', 'bg_bright': '', 'bg_lmode': '', 'bg_rgb': '', 'bg_hue': '', 'bg_sat': '', 'bg_ct': '', 'bg_flowing': '', 'bg_flow_params': ''}
# yeelink.light.color3, yeelink.light.color4, yeelink.light.color5, yeelink.light.strip2
# {'name': '', 'lan_ctrl': '1', 'save_state': '1', 'delayoff': '0', 'music_on': '0', 'power': 'off', 'bright': '100', 'color_mode': '1', 'rgb': '2353663', 'hue': '186', 'sat': '86', 'ct': '6500', 'flowing': '0', 'flow_params': '0,0,1000,1,16711680,100,1000,1,65280,100,1000,1,255,100', 'active_mode': '', 'nl_br': '', 'bg_power': '', 'bg_bright': '', 'bg_lmode': '', 'bg_rgb': '', 'bg_hue': '', 'bg_sat': '', 'bg_ct': '', 'bg_flowing': '', 'bg_flow_params': ''}
self.data = data

@property
def developer_mode(self) -> Optional[bool]:
"""Return whether the developer mode is active."""
lan_ctrl = self.data["lan_ctrl"]
if lan_ctrl:
Expand All @@ -85,6 +144,48 @@ def name(self) -> str:
"""Return the internal name of the bulb."""
return self.data["name"]

@property
def delay_off(self) -> int:
"""Return delay in minute before bulb is off."""
return int(self.data["delayoff"])

@property
def music_mode(self) -> Optional[bool]:
"""Return whether the music mode is active."""
music_on = self.data["music_on"]
if music_on:
return bool(int(music_on))
return None

@property
def moonlight_mode(self) -> Optional[bool]:
"""Return whether the moonlight mode is active."""
active_mode = self.data["active_mode"]
if active_mode:
return bool(int(active_mode))
return None

@property
def moonlight_mode_brightness(self) -> Optional[int]:
"""Return current moonlight brightness."""
nl_br = self.data["nl_br"]
if nl_br:
return int(self.data["nl_br"])
return None

@property
def lights(self) -> List[YeelightSubLight]:
"""Return list of sub lights."""
sub_lights = list({YeelightSubLight(self.data, YeelightSubLightType.Main)})
bg_power = self.data[
"bg_power"
] # to do: change this to model spec in the future.
if bg_power:
sub_lights.append(
YeelightSubLight(self.data, YeelightSubLightType.Background)
)
return sub_lights


class Yeelight(Device):
"""A rudimentary support for Yeelight bulbs.
Expand All @@ -109,30 +210,49 @@ def __init__(self, *args, **kwargs):
default_output=format_output(
"",
"Name: {result.name}\n"
"Power: {result.is_on}\n"
"Brightness: {result.brightness}\n"
"Color mode: {result.color_mode}\n"
"RGB: {result.rgb}\n"
"HSV: {result.hsv}\n"
"Temperature: {result.color_temp}\n"
"Developer mode: {result.developer_mode}\n"
"Update default on change: {result.save_state_on_change}\n"
"Delay in minute before off: {result.delay_off}\n"
"Music mode: {result.music_mode}\n"
"Lights: \n{result.lights}\n"
"Moonlight\n"
" Is in mode: {result.moonlight_mode}\n"
" Moonlight mode brightness: {result.moonlight_mode_brightness}\n"
"\n",
)
)
def status(self) -> YeelightStatus:
"""Retrieve properties."""
properties = [
# general properties
"name",
"lan_ctrl",
"save_state",
"delayoff",
"music_on",
# light properties
"power",
"bright",
"ct",
"color_mode",
"rgb",
"hue",
"sat",
"color_mode",
"name",
"lan_ctrl",
"save_state",
"ct",
"flowing",
"flow_params",
# moonlight properties
"active_mode",
"nl_br",
# background light properties
"bg_power",
"bg_bright",
"bg_lmode",
"bg_rgb",
"bg_hue",
"bg_sat",
"bg_ct",
"bg_flowing",
"bg_flow_params",
rytilahti marked this conversation as resolved.
Show resolved Hide resolved
]

values = self.get_properties(properties)
Expand Down