diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..09a834066 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +sudo: false +language: python +python: + - "3.4" + - "3.5" + - "3.6" +install: pip install tox-travis +script: tox diff --git a/mirobo/airpurifier.py b/mirobo/airpurifier.py index e10515e28..d45bc0ca7 100644 --- a/mirobo/airpurifier.py +++ b/mirobo/airpurifier.py @@ -137,6 +137,7 @@ def humidity(self) -> int: def temperature(self) -> float: if self.data["temp_dec"] is not None: return self.data["temp_dec"] / 10.0 + return None @property def mode(self) -> OperationMode: @@ -150,6 +151,7 @@ def led(self) -> bool: def led_brightness(self) -> LedBrightness: if self.data["led_b"] is not None: return LedBrightness(self.data["led_b"]) + return None @property def buzzer(self) -> bool: diff --git a/mirobo/ceil.py b/mirobo/ceil.py index 24ea4df4e..31045568b 100644 --- a/mirobo/ceil.py +++ b/mirobo/ceil.py @@ -50,10 +50,10 @@ def automatic_color_temperature(self) -> int: def __str__(self) -> str: s = "" % \ - (self.power, self.brightness, - self.color_temperature, self.scene, self.dv, - self.smart_night_light, self.automatic_color_temperature) + "smart_night_light=%s, automatic_color_temperature=%s>" % ( + self.power, self.brightness, + self.color_temperature, self.scene, self.dv, + self.smart_night_light, self.automatic_color_temperature) return s diff --git a/mirobo/device.py b/mirobo/device.py index 6db94f05e..8eac3fd1d 100644 --- a/mirobo/device.py +++ b/mirobo/device.py @@ -23,6 +23,7 @@ def __repr__(self): self.data["mac"], self.netif["localIp"], self.data["token"]) + @property def netif(self): return self.data["netif"] @@ -170,7 +171,8 @@ def send(self, command: str, parameters: Any=None, retry_count=3) -> Any: except OSError as ex: _LOGGER.error("Got error when receiving: %s", ex) if retry_count > 0: - _LOGGER.warning("Retrying with incremented id, retries left: %s" % retry_count) + _LOGGER.warning("Retrying with incremented id, " + "retries left: %s" % retry_count) self.__id += 100 return self.send(command, parameters, retry_count-1) raise DeviceException from ex diff --git a/mirobo/discovery.py b/mirobo/discovery.py index 437b7ba90..3529cabbe 100644 --- a/mirobo/discovery.py +++ b/mirobo/discovery.py @@ -4,13 +4,17 @@ import inspect import codecs from mirobo import (Device, Vacuum, Plug, PlugV1, Strip, AirPurifier, Ceil, - PhilipsEyecare, ) + PhilipsEyecare) +from typing import Union, Callable, Dict _LOGGER = logging.getLogger(__name__) def other_package_info(info, desc): - return "%s @ %s, check %s" % (info.name, ipaddress.ip_address(info.address), desc) + return "%s @ %s, check %s" % ( + info.name, + ipaddress.ip_address(info.address), + desc) class Listener: diff --git a/mirobo/philips_eyecare.py b/mirobo/philips_eyecare.py index eb28590f0..ee2dcf40e 100644 --- a/mirobo/philips_eyecare.py +++ b/mirobo/philips_eyecare.py @@ -129,6 +129,5 @@ def __str__(self) -> str: "dvalue=%s >" % \ (self.power, self.bright, self.notifystatus, self.ambstatus, self.ambvalue, - self.eyecare, self.scene_num, - self.bls) + self.eyecare, self.scene_num, self.bls, self.dvalue) return s diff --git a/mirobo/plug.py b/mirobo/plug.py index 9fab5e265..7d6e1fba5 100644 --- a/mirobo/plug.py +++ b/mirobo/plug.py @@ -25,6 +25,7 @@ def load_power(self) -> float: # The constant of 110V is used intentionally. The current was # calculated with a wrong reference voltage already. return self.data["current"] * 110 + return None def __str__(self) -> str: s = "" % \ diff --git a/mirobo/plug_cli.py b/mirobo/plug_cli.py index 34335c967..707ac3ea3 100644 --- a/mirobo/plug_cli.py +++ b/mirobo/plug_cli.py @@ -4,6 +4,7 @@ import ast import sys import ipaddress +from typing import Any if sys.version_info < (3, 4): print("To use this script you need python 3.4 or newer, got %s" % diff --git a/mirobo/protocol.py b/mirobo/protocol.py index 0119c5053..529ff3f88 100644 --- a/mirobo/protocol.py +++ b/mirobo/protocol.py @@ -34,6 +34,13 @@ class Utils: """ This class is adapted from the original xpn.py code by gst666 """ + @staticmethod + def verify_token(token: bytes): + if not isinstance(token, bytes): + raise TypeError("Token must be bytes") + if len(token) != 32: + raise ValueError("Token must be of length 32") + @staticmethod def md5(data: bytes) -> bytes: checksum = hashlib.md5() @@ -48,6 +55,9 @@ def key_iv(token: bytes) -> Tuple[bytes, bytes]: @staticmethod def encrypt(plaintext: bytes, token: bytes) -> bytes: + if not isinstance(plaintext, bytes): + raise TypeError("plaintext requires bytes") + Utils.verify_token(token) key, iv = Utils.key_iv(token) padder = padding.PKCS7(128).padder() @@ -60,6 +70,9 @@ def encrypt(plaintext: bytes, token: bytes) -> bytes: @staticmethod def decrypt(ciphertext: bytes, token: bytes) -> bytes: + if not isinstance(ciphertext, bytes): + raise TypeError("ciphertext requires bytes") + Utils.verify_token(token) key, iv = Utils.key_iv(token) cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) diff --git a/mirobo/tests/__init__.py b/mirobo/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mirobo/tests/test_protocol.py b/mirobo/tests/test_protocol.py new file mode 100644 index 000000000..c736c4835 --- /dev/null +++ b/mirobo/tests/test_protocol.py @@ -0,0 +1,34 @@ +from unittest import TestCase +from .. import Message, Utils + + +class TestProtocol(TestCase): + def test_non_bytes_payload(self): + payload = "hello world" + valid_token = 32 * b'0' + with self.assertRaises(TypeError): + Utils.encrypt(payload, valid_token) + with self.assertRaises(TypeError): + Utils.decrypt(payload, valid_token) + + def test_encrypt(self): + payload = b"hello world" + token = 32 * b'0' + + encrypted = Utils.encrypt(payload, token) + decrypted = Utils.decrypt(encrypted, token) + self.assertEquals(payload, decrypted) + + def test_invalid_token(self): + payload = b"hello world" + wrong_type = 1234 + wrong_length = 31 * b'0' + with self.assertRaises(TypeError): + Utils.encrypt(payload, wrong_type) + with self.assertRaises(TypeError): + Utils.decrypt(payload, wrong_type) + + with self.assertRaises(ValueError): + Utils.encrypt(payload, wrong_length) + with self.assertRaises(ValueError): + Utils.decrypt(payload, wrong_length) diff --git a/mirobo/vacuum_cli.py b/mirobo/vacuum_cli.py index f7c8df65f..649c09e21 100644 --- a/mirobo/vacuum_cli.py +++ b/mirobo/vacuum_cli.py @@ -250,7 +250,7 @@ def backward(vac: mirobo.Vacuum, amount:float): @click.argument('rotation', type=float) @click.argument('velocity', type=float) @click.argument('duration', type=int) -def move(vac: mirobo.Vacuum, rotation: float, velocity: float, duration: int): +def move(vac: mirobo.Vacuum, rotation: int, velocity: float, duration: int): """Pass raw manual values""" return vac.manual_control(rotation, velocity, duration) diff --git a/mirobo/yeelight.py b/mirobo/yeelight.py index 39decb486..39ff16361 100644 --- a/mirobo/yeelight.py +++ b/mirobo/yeelight.py @@ -46,6 +46,7 @@ def rgb(self): def color_temp(self) -> int: if self.color_mode == YeelightMode.ColorTemperature: return int(self.data["ct"]) + return None @property def name(self) -> str: diff --git a/tox.ini b/tox.ini index 18dcf91d0..54210fde2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,21 @@ [tox] -envlist=flake8,typing +envlist=py34,py35,py36,flake8,typing -#[tox:travis] -#3.4 = py34 -#3.5 = py35 -#3.6 = py36 +[tox:travis] +3.4 = py34 +3.5 = py35 +3.6 = py36 -#[testenv] -#deps= -#commands=py.test mirobo +[testenv] +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH +deps= + pytest + pytest-cov + voluptuous + coveralls +commands= + py.test --cov mirobo + coveralls [testenv:flake8] deps=flake8