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 1 commit
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]
56 changes: 56 additions & 0 deletions miio/tests/test_plug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from unittest import TestCase
from miio import Plug
from .dummies import DummyDevice
import pytest

class DummyPlug(DummyDevice, Plug):

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

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

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

def plug(request):
request.cls.device = DummyPlug()
# TODO add ability to test on a real device

@pytest.mark.usefixtures("plug")

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1


class TestPlug(TestCase):
def test_on(self):
self.device.off() # ensure off
is_on = lambda: self.device.status().is_on

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def

start_state = is_on()
assert start_state == False

Choose a reason for hiding this comment

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

comparison to False should be 'if cond is False:' or 'if not cond:'


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

Choose a reason for hiding this comment

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

comparison to True should be 'if cond is True:' or 'if cond:'



def test_off(self):

Choose a reason for hiding this comment

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

too many blank lines (2)

Choose a reason for hiding this comment

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

too many blank lines (2)

Choose a reason for hiding this comment

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

too many blank lines (2)

self.device.on() # ensure on
is_on = lambda: self.device.status().is_on

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def

assert is_on() == True

Choose a reason for hiding this comment

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

comparison to True should be 'if cond is True:' or 'if cond:'


self.device.off()
assert is_on() == False

Choose a reason for hiding this comment

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

comparison to False should be 'if cond is False:' or 'if not cond:'


def test_status(self):
self.device._reset_state()
state = lambda: self.device.status()

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def

print(state())
assert state().is_on == True

Choose a reason for hiding this comment

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

comparison to True should be 'if cond is True:' or 'if cond:'

assert state().temperature == self.device.start_state["temperature"]
assert 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.

multiple spaces after operator

Choose a reason for hiding this comment

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

multiple spaces after operator

Choose a reason for hiding this comment

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

multiple spaces after operator


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

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def

assert state().load_power is None
179 changes: 179 additions & 0 deletions miio/tests/test_yeelight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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):

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

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

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

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 == False

Choose a reason for hiding this comment

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

comparison to False should be 'if cond is False:' or 'if not cond:'

assert status.brightness == 100
assert status.color_temp == 3584
assert status.color_mode == YeelightMode.ColorTemperature
assert status.developer_mode == True

Choose a reason for hiding this comment

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

comparison to True should be 'if cond is True:' or 'if cond:'

assert status.save_state_on_change == True

Choose a reason for hiding this comment

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

comparison to True should be 'if cond is True:' or 'if cond:'


# 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

Choose a reason for hiding this comment

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

at least two spaces before inline comment

assert self.device.status().is_on == False

Choose a reason for hiding this comment

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

comparison to False should be 'if cond is False:' or 'if not cond:'

self.device.on()
assert self.device.status().is_on == True

Choose a reason for hiding this comment

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

comparison to True should be 'if cond is True:' or 'if cond:'


def test_off(self):
self.device.on() # make sure we are on

Choose a reason for hiding this comment

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

at least two spaces before inline comment

assert self.device.status().is_on == True

Choose a reason for hiding this comment

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

comparison to True should be 'if cond is True:' or 'if cond:'

self.device.off()
assert self.device.status().is_on == False

Choose a reason for hiding this comment

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

comparison to False should be 'if cond is False:' or 'if not cond:'


def test_set_brightness(self):
brightness = lambda: self.device.status().brightness

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def


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):
color_temp = lambda: self.device.status().color_temp

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def

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):
dev_mode = lambda: self.device.status().developer_mode

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def


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):
save_state = lambda: self.device.status().save_state_on_change

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def

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):
name = lambda: self.device.status().name

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def


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

def test_toggle(self):
is_on = lambda: self.device.status().is_on

Choose a reason for hiding this comment

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

do not assign a lambda expression, use a def


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