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

Add a base to allow easier testing of devices #99

Merged
merged 2 commits into from
Oct 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions miio/tests/dummies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class DummyDevice:
"""DummyDevice base class, you should inherit from this and call
`super().__init__(args, kwargs)` to save the original state.

This class provides helpers to test simple devices, for more complex
ones you will want to extend the `return_values` accordingly.
The basic idea is that the overloaded send() will read a wanted response
based on the call from `return_values`.

For changing values :func:`_set_state` will use :func:`pop()` to extract
the first parameter and set the state accordingly.

For a very simple device the following is enough, see :class:`TestPlug`
for complete code.

.. code-block::
self.return_values = {
"get_prop": self._get_state,
"power": lambda x: self._set_state("power", x)
}

"""
def __init__(self, *args, **kwargs):
self.start_state = self.state.copy()

def send(self, command: str, parameters=None, retry_count=3):
"""Overridden send() to return values from `self.return_values`."""
return self.return_values[command](parameters)

def _reset_state(self):
"""Revert back to the original state."""
self.state = self.start_state.copy()

def _set_state(self, var, value):
"""Set a state of a variable,
the value is expected to be an array with length of 1."""
# print("setting %s = %s" % (var, value))
self.state[var] = value.pop(0)

def _get_state(self, props):
"""Return wanted properties"""
return [self.state[x] for x in props if x in self.state]
61 changes: 61 additions & 0 deletions miio/tests/test_plug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from unittest import TestCase
from miio import Plug
from .dummies import DummyDevice
import pytest


class DummyPlug(DummyDevice, Plug):
def __init__(self, *args, **kwargs):
self.state = {
'power': 'on',
'temperature': 32,
'current': 123,
}
self.return_values = {
'get_prop': self._get_state,
'set_power': lambda x: self._set_state("power", x),
}
super().__init__(args, kwargs)


@pytest.fixture(scope="class")
def plug(request):
request.cls.device = DummyPlug()
# TODO add ability to test on a real device


@pytest.mark.usefixtures("plug")
class TestPlug(TestCase):
def is_on(self):
return self.device.status().is_on

def state(self):
return self.device.status()

def test_on(self):
self.device.off() # ensure off

start_state = self.is_on()
assert start_state is False

self.device.on()
assert self.is_on() is True

def test_off(self):
self.device.on() # ensure on

assert self.is_on() is True
self.device.off()
assert self.is_on() is False

def test_status(self):
self.device._reset_state()

assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temperature"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (81 > 79 characters)

assert self.state().load_power == self.device.start_state["current"] * 110

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (82 > 79 characters)


def test_status_without_current(self):
del self.device.state["current"]

assert self.state().load_power is None
189 changes: 189 additions & 0 deletions miio/tests/test_yeelight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
from unittest import TestCase
from miio import Yeelight
from miio.yeelight import YeelightMode, YeelightStatus, YeelightException

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'miio.yeelight.YeelightStatus' imported but unused

import pytest
from .dummies import DummyDevice


class DummyLight(DummyDevice, Yeelight):
def __init__(self, *args, **kwargs):
self.state = {
'power': 'off',
'bright': '100',
'ct': '3584',
'rgb': '16711680',
'hue': '359',
'sat': '100',
'color_mode': '2',
'name': 'test name',
'lan_ctrl': '1',
'save_state': '1'
}

self.return_values = {
'get_prop': self._get_state,
'set_power': lambda x: self._set_state("power", x),
'set_bright': lambda x: self._set_state("bright", x),
'set_ct_abx': lambda x: self._set_state("ct", x),
'set_rgb': lambda x: self._set_state("rgb", x),
'set_hsv': lambda x: self._set_state("hsv", x),
'set_name': lambda x: self._set_state("name", x),
'set_ps': lambda x: self.set_config(x),
'toggle': self.toggle_power,
'set_default': lambda x: 'ok'
}

super().__init__(*args, **kwargs)

def set_config(self, x):
key, value = x
config_mapping = {
'cfg_lan_ctrl': 'lan_ctrl',
'cfg_save_state': 'save_state'
}

self._set_state(config_mapping[key], [value])

def toggle_power(self, _):
if self.state["power"] == "on":
self.state["power"] = "off"
else:
self.state["power"] = "on"


@pytest.fixture(scope="class")
def dummylight(request):
request.cls.device = DummyLight()
# TODO add ability to test on a real device


@pytest.mark.usefixtures("dummylight")
class TestYeelight(TestCase):
def test_status(self):
self.device._reset_state()
status = self.device.status() # type: YeelightStatus
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.developer_mode is True
assert status.save_state_on_change is True

# following are tested in set mode tests
# assert status.rgb == 16711680
# assert status.hsv == (359, 100, 100)

def test_on(self):
self.device.off() # make sure we are off
assert self.device.status().is_on is False
self.device.on()
assert self.device.status().is_on is True

def test_off(self):
self.device.on() # make sure we are on
assert self.device.status().is_on is True
self.device.off()
assert self.device.status().is_on is False

def test_set_brightness(self):
def brightness():
return self.device.status().brightness

self.device.set_brightness(50)
assert brightness() == 50
self.device.set_brightness(0)
assert brightness() == 0
self.device.set_brightness(100)

with pytest.raises(YeelightException):
self.device.set_brightness(-100)

with pytest.raises(YeelightException):
self.device.set_brightness(200)

def test_set_color_temp(self):
def color_temp():
return self.device.status().color_temp

self.device.set_color_temp(2000)
assert color_temp() == 2000
self.device.set_color_temp(6500)
assert color_temp() == 6500

with pytest.raises(YeelightException):
self.device.set_color_temp(1000)

with pytest.raises(YeelightException):
self.device.set_color_temp(7000)

@pytest.mark.skip("rgb is not properly implemented")
def test_set_rgb(self):
self.device._reset_state()
assert self.device.status().rgb == 16711680

NEW_RGB = 16712222
self.set_rgb(NEW_RGB)
assert self.device.status().rgb == NEW_RGB

@pytest.mark.skip("hsv is not properly implemented")
def test_set_hsv(self):
self.reset_state()
hue, sat, val = self.device.status().hsv
assert hue == 359
assert sat == 100
assert val == 100

self.device.set_hsv()

def test_set_developer_mode(self):
def dev_mode():
return self.device.status().developer_mode

orig_mode = dev_mode()
self.device.set_developer_mode(not orig_mode)
new_mode = dev_mode()
assert new_mode is not orig_mode
self.device.set_developer_mode(not new_mode)
assert new_mode is not dev_mode()

def test_set_save_state_on_change(self):
def save_state():
return self.device.status().save_state_on_change

orig_state = save_state()
self.device.set_save_state_on_change(not orig_state)
new_state = save_state()
assert new_state is not orig_state
self.device.set_save_state_on_change(not new_state)
new_state = save_state()
assert new_state is orig_state

def test_set_name(self):
def name():
return self.device.status().name

assert name() == "test name"
self.device.set_name("new test name")
assert name() == "new test name"

def test_toggle(self):
def is_on():
return self.device.status().is_on

orig_state = is_on()
self.device.toggle()
new_state = is_on()
assert orig_state != new_state

self.device.toggle()
new_state = is_on()
assert new_state == orig_state

@pytest.mark.skip("cannot be tested easily")
def test_set_default(self):
self.fail()

@pytest.mark.skip("set_scene is not implemented")
def test_set_scene(self):
self.fail()
10 changes: 9 additions & 1 deletion miio/yeelight.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import warnings


class YeelightException(Exception):
pass


class YeelightMode(IntEnum):
RGB = 1
ColorTemperature = 2
Expand Down Expand Up @@ -145,10 +149,14 @@ def off(self):

def set_brightness(self, bright):
"""Set brightness."""
if bright < 0 or bright > 100:
raise YeelightException("Invalid brightness: %s" % bright)
return self.send("set_bright", [bright])

def set_color_temp(self, ct):
"""Set color temp in kelvin."""
if ct > 6500 or ct < 1700:
raise YeelightException("Invalid color temperature: %s" % ct)
return self.send("set_ct_abx", [ct, "smooth", 500])

def set_rgb(self, rgb):
Expand All @@ -165,7 +173,7 @@ def set_developer_mode(self, enable: bool) -> bool:

def set_save_state_on_change(self, enable: bool) -> bool:
"""Enable or disable saving the state on changes."""
return self.send("set_ps", ["cfg_save_state"], str(int(enable)))
return self.send("set_ps", ["cfg_save_state", str(int(enable))])

def set_name(self, name: str) -> bool:
"""Set an internal name for the bulb."""
Expand Down