-
-
Notifications
You must be signed in to change notification settings - Fork 563
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added support of Yeelight Dual Control Module (yeelink.switch.sw1)
- Loading branch information
Ihor Syerkov
authored and
Ihor Syerkov
committed
Jan 8, 2021
1 parent
64040d1
commit ae5c854
Showing
5 changed files
with
366 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
from unittest import TestCase | ||
|
||
import pytest | ||
|
||
from miio import YeelightDualControlModule | ||
from miio.yeelight_dual_switch import Switch, YeelightDualControlModuleException | ||
|
||
from .dummies import DummyMiotDevice | ||
|
||
_INITIAL_STATE = { | ||
"switch_1_state": True, | ||
"switch_1_default_state": True, | ||
"switch_1_off_delay": 300, | ||
"switch_2_state": False, | ||
"switch_2_default_state": False, | ||
"switch_2_off_delay": 0, | ||
"interlock": False, | ||
"flex_mode": True, | ||
"rc_list": "[{'mac':'9db0eb4124f8','evtid':4097,'pid':339,'beaconkey':'3691bc0679eef9596bb63abf'}]", | ||
} | ||
|
||
|
||
class DummyYeelightDualControlModule(DummyMiotDevice, YeelightDualControlModule): | ||
def __init__(self, *args, **kwargs): | ||
self.state = _INITIAL_STATE | ||
self.return_values = { | ||
"get_prop": self._get_state, | ||
} | ||
super().__init__(*args, **kwargs) | ||
|
||
|
||
@pytest.fixture(scope="function") | ||
def switch(request): | ||
request.cls.device = DummyYeelightDualControlModule() | ||
|
||
|
||
@pytest.mark.usefixtures("switch") | ||
class TestYeelightDualControlModule(TestCase): | ||
def test_1_on(self): | ||
self.device.off(Switch.First) # ensure off | ||
print(self.device.status()) | ||
assert self.device.status().switch_1_state is False | ||
|
||
self.device.on(Switch.First) | ||
assert self.device.status().switch_1_state is True | ||
|
||
def test_2_on(self): | ||
self.device.off(Switch.Second) # ensure off | ||
assert self.device.status().switch_2_state is False | ||
|
||
self.device.on(Switch.Second) | ||
assert self.device.status().switch_2_state is True | ||
|
||
def test_1_off(self): | ||
self.device.on(Switch.First) # ensure on | ||
assert self.device.status().switch_1_state is True | ||
|
||
self.device.off(Switch.First) | ||
assert self.device.status().switch_1_state is False | ||
|
||
def test_2_off(self): | ||
self.device.on(Switch.Second) # ensure on | ||
assert self.device.status().switch_2_state is True | ||
|
||
self.device.off(Switch.Second) | ||
assert self.device.status().switch_2_state is False | ||
|
||
def test_status(self): | ||
status = self.device.status() | ||
|
||
assert status.switch_1_state is _INITIAL_STATE["switch_1_state"] | ||
assert status.switch_1_off_delay == _INITIAL_STATE["switch_1_off_delay"] | ||
assert status.switch_1_default_state == _INITIAL_STATE["switch_1_default_state"] | ||
assert status.switch_1_state is _INITIAL_STATE["switch_1_state"] | ||
assert status.switch_1_off_delay == _INITIAL_STATE["switch_1_off_delay"] | ||
assert status.switch_1_default_state == _INITIAL_STATE["switch_1_default_state"] | ||
assert status.interlock == _INITIAL_STATE["interlock"] | ||
assert status.flex_mode == _INITIAL_STATE["flex_mode"] | ||
assert status.rc_list == _INITIAL_STATE["rc_list"] | ||
|
||
def test_set_switch_off_delay(self): | ||
self.device.set_switch_off_delay(300, Switch.First) | ||
assert self.device.status().switch_1_off_delay == 300 | ||
self.device.set_switch_off_delay(200, Switch.Second) | ||
assert self.device.status().switch_2_off_delay == 200 | ||
|
||
with pytest.raises(YeelightDualControlModuleException): | ||
self.device.set_switch_off_delay(-2, Switch.First) | ||
|
||
with pytest.raises(YeelightDualControlModuleException): | ||
self.device.set_switch_off_delay(43300, Switch.Second) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
import enum | ||
from typing import Any, Dict | ||
|
||
import click | ||
|
||
from .click_common import EnumType, command, format_output | ||
from .exceptions import DeviceException | ||
from .miot_device import MiotDevice | ||
|
||
|
||
class YeelightDualControlModuleException(DeviceException): | ||
pass | ||
|
||
|
||
class Switch(enum.Enum): | ||
First = 0 | ||
Second = 1 | ||
|
||
|
||
_MAPPING = { | ||
# http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:switch:0000A003:yeelink-sw1:1:0000C809 | ||
# First Switch (siid=2) | ||
"switch_1_state": {"siid": 2, "piid": 1}, # bool | ||
"switch_1_default_state": {"siid": 2, "piid": 2}, # 0 - Off, 1 - On | ||
"switch_1_off_delay": {"siid": 2, "piid": 3}, # -1 - Off, [1, 43200] - delay in sec | ||
# Second Switch (siid=3) | ||
"switch_2_state": {"siid": 3, "piid": 1}, # bool | ||
"switch_2_default_state": {"siid": 3, "piid": 2}, # 0 - Off, 1 - On | ||
"switch_2_off_delay": {"siid": 3, "piid": 3}, # -1 - Off, [1, 43200] - delay in sec | ||
# Extensions (siid=4) | ||
"interlock": {"siid": 4, "piid": 1}, # bool | ||
"flex_mode": {"siid": 4, "piid": 2}, # 0 - Off, 1 - On | ||
"rc_list": {"siid": 4, "piid": 3}, # string | ||
"rc_list_for_del": {"siid": 4, "piid": 4}, # string | ||
"toggle": {"siid": 4, "piid": 5}, # 0 - First switch, 1 - Second switch | ||
} | ||
|
||
|
||
class DualControlModuleStatus: | ||
def __init__(self, data: Dict[str, Any]) -> None: | ||
""" | ||
Response of Yeelight Dual Control Module | ||
{ | ||
'id': 1, | ||
'result': [ | ||
{'did': 'switch_1_state', 'siid': 2, 'piid': 1, 'code': 0, 'value': False}, | ||
{'did': 'switch_1_default_state', 'siid': 2, 'piid': 2, 'code': 0, 'value': True}, | ||
{'did': 'switch_1_off_delay', 'siid': 2, 'piid': 3, 'code': 0, 'value': 300}, | ||
{'did': 'switch_2_state', 'siid': 3, 'piid': 1, 'code': 0, 'value': False}, | ||
{'did': 'switch_2_default_state', 'siid': 3, 'piid': 2, 'code': 0, 'value': False}, | ||
{'did': 'switch_2_off_delay', 'siid': 3, 'piid': 3, 'code': 0, 'value': 0}, | ||
{'did': 'interlock', 'siid': 4, 'piid': 1, 'code': 0, 'value': False}, | ||
{'did': 'flex_mode', 'siid': 4, 'piid': 2, 'code': 0, 'value': True}, | ||
{'did': 'rc_list', 'siid': 4, 'piid': 2, 'code': 0, 'value': '[{"mac":"9db0eb4124f8","evtid":4097,"pid":339,"beaconkey":"3691bc0679eef9596bb63abf"}]'}, | ||
] | ||
} | ||
""" | ||
self.data = data | ||
|
||
@property | ||
def switch_1_state(self) -> bool: | ||
"""First switch state""" | ||
return bool(self.data["switch_1_state"]) | ||
|
||
@property | ||
def switch_1_default_state(self) -> bool: | ||
"""First switch default state""" | ||
return bool(self.data["switch_1_default_state"]) | ||
|
||
@property | ||
def switch_1_off_delay(self) -> int: | ||
"""First switch off delay""" | ||
return self.data["switch_1_off_delay"] | ||
|
||
@property | ||
def switch_2_state(self) -> bool: | ||
"""Second switch state""" | ||
return bool(self.data["switch_2_state"]) | ||
|
||
@property | ||
def switch_2_default_state(self) -> bool: | ||
"""Second switch default state""" | ||
return bool(self.data["switch_2_default_state"]) | ||
|
||
@property | ||
def switch_2_off_delay(self) -> int: | ||
"""Second switch off delay""" | ||
return self.data["switch_2_off_delay"] | ||
|
||
@property | ||
def interlock(self) -> bool: | ||
"""Interloc""" | ||
return bool(self.data["interlock"]) | ||
|
||
@property | ||
def flex_mode(self) -> int: | ||
"""Flex mode""" | ||
return self.data["flex_mode"] | ||
|
||
@property | ||
def rc_list(self) -> str: | ||
"""List of paired remote controls""" | ||
return self.data["rc_list"] | ||
|
||
def __repr__(self) -> str: | ||
s = ( | ||
"<YeelightDualControlModuleStatus " | ||
"switch_1_state=%s, " | ||
"switch_1_default_state=%s, " | ||
"switch_1_off_delay=%s, " | ||
"switch_2_state=%s, " | ||
"switch_2_default_state=%s, " | ||
"switch_2_off_delay=%s, " | ||
"interlock=%s, " | ||
"flex_mode=%s, " | ||
"rc_list=%s>" | ||
% ( | ||
self.switch_1_state, | ||
self.switch_1_default_state, | ||
self.switch_1_off_delay, | ||
self.switch_2_state, | ||
self.switch_2_default_state, | ||
self.switch_2_off_delay, | ||
self.interlock, | ||
self.flex_mode, | ||
self.rc_list, | ||
) | ||
) | ||
return s | ||
|
||
|
||
class YeelightDualControlModule(MiotDevice): | ||
"""Main class representing the Yeelight Dual Control Module (yeelink.switch.sw1) which uses MIoT protocol.""" | ||
|
||
def __init__( | ||
self, | ||
ip: str = None, | ||
token: str = None, | ||
start_id: int = 0, | ||
debug: int = 0, | ||
lazy_discover: bool = True, | ||
) -> None: | ||
super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover) | ||
|
||
@command( | ||
default_output=format_output( | ||
"", | ||
"First Switch Status: {result.switch_1_state}\n" | ||
"First Switch Default State: {result.switch_1_default_state}\n" | ||
"First Switch Delay: {result.switch_1_off_delay}\n" | ||
"Second Switch Status: {result.switch_2_state}\n" | ||
"Second Switch Default State: {result.switch_2_default_state}\n" | ||
"Second Switch Delay: {result.switch_2_off_delay}\n" | ||
"Interlock: {result.interlock}\n" | ||
"Flex Mode: {result.flex_mode}\n" | ||
"RC list: {result.rc_list}\n", | ||
) | ||
) | ||
def status(self) -> DualControlModuleStatus: | ||
"""Retrieve properties""" | ||
p = [ | ||
"switch_1_state", | ||
"switch_1_default_state", | ||
"switch_1_off_delay", | ||
"switch_2_state", | ||
"switch_2_default_state", | ||
"switch_2_off_delay", | ||
"interlock", | ||
"flex_mode", | ||
"rc_list", | ||
] | ||
"""Filter only readable properties for status""" | ||
properties = [ | ||
{"did": k, **v} | ||
for k, v in filter(lambda item: item[0] in p, _MAPPING.items()) | ||
] | ||
values = self.get_properties(properties) | ||
return DualControlModuleStatus( | ||
dict(map(lambda v: (v["did"], v["value"]), values)) | ||
) | ||
|
||
@command( | ||
click.argument("switch", type=EnumType(Switch)), | ||
default_output=format_output("Turn {switch} switch on"), | ||
) | ||
def on(self, switch: Switch): | ||
"""Turn switch on.""" | ||
if switch == Switch.First: | ||
return self.set_property("switch_1_state", True) | ||
elif switch == Switch.Second: | ||
return self.set_property("switch_2_state", True) | ||
|
||
@command( | ||
click.argument("switch", type=EnumType(Switch)), | ||
default_output=format_output("Turn {switch} switch off"), | ||
) | ||
def off(self, switch: Switch): | ||
"""Turn switch off.""" | ||
if switch == Switch.First: | ||
return self.set_property("switch_1_state", False) | ||
elif switch == Switch.Second: | ||
return self.set_property("switch_2_state", False) | ||
|
||
@command( | ||
click.argument("switch", type=EnumType(Switch)), | ||
default_output=format_output("Toggle {switch} switch"), | ||
) | ||
def toggle(self, switch: Switch): | ||
"""Toggle switch.""" | ||
return self.set_property("toggle", switch.value) | ||
|
||
@command( | ||
click.argument("state", type=bool), | ||
click.argument("switch", type=EnumType(Switch)), | ||
default_output=format_output("Set {switch} switch default state to: {state}"), | ||
) | ||
def set_default_state(self, state: bool, switch: Switch): | ||
"""Set switch default state.""" | ||
if switch == Switch.First: | ||
return self.set_property("switch_1_default_state", int(state)) | ||
elif switch == Switch.Second: | ||
return self.set_property("switch_2_default_state", int(state)) | ||
|
||
@command( | ||
click.argument("delay", type=int), | ||
click.argument("switch", type=EnumType(Switch)), | ||
default_output=format_output("Set {switch} switch off delay to {delay} sec."), | ||
) | ||
def set_switch_off_delay(self, delay: int, switch: Switch): | ||
"""Set switch off delay, should be between -1 to 43200 (in seconds)""" | ||
if delay < -1 or delay > 43200: | ||
raise YeelightDualControlModuleException( | ||
"Invalid switch delay: %s (should be between -1 to 43200)" % delay | ||
) | ||
|
||
if switch == Switch.First: | ||
return self.set_property("switch_1_off_delay", delay) | ||
elif switch == Switch.Second: | ||
return self.set_property("switch_2_off_delay", delay) | ||
|
||
@command( | ||
click.argument("flex_mode", type=bool), | ||
default_output=format_output("Set flex mode to: {flex_mode}"), | ||
) | ||
def set_flex_mode(self, flex_mode: bool): | ||
"""Set flex mode.""" | ||
return self.set_property("flex_mode", int(flex_mode)) | ||
|
||
@command( | ||
click.argument("rc_mac", type=str), | ||
default_output=format_output("Delete remote control with MAC: {rc_mac}"), | ||
) | ||
def delete_rc(self, rc_mac: str): | ||
"""Delete remote control by MAC""" | ||
return self.set_property("rc_list_for_del", rc_mac) | ||
|
||
@command( | ||
click.argument("interlock", type=bool), | ||
default_output=format_output("Set interlock to: {interlock}"), | ||
) | ||
def set_interlock(self, interlock: bool): | ||
"""Set interlock""" | ||
return self.set_property("interlock", interlock) |