From f2c749aa274f6b571e7d57d41a36770b8aa09419 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 10 May 2018 18:32:32 +0200 Subject: [PATCH 01/14] Air Conditioning Companion: Rewrite a captured command before replay --- miio/airconditioningcompanion.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index 210ce57e9..7b43abc07 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -306,14 +306,29 @@ def learn_stop(self, slot: int=STORAGE_SLOT_ID): return self.send("end_ir_learn", [slot]) @command( - click.argument("command", type=str), + click.argument("model", type=str), + click.argument("code", type=str), default_output=format_output("Sending the supplied infrared command") ) - def send_ir_code(self, command: str): + def send_ir_code(self, model: str, code: str): """Play a captured command. - :param str command: Command to execute""" - return self.send("send_ir_code", [str(command)]) + :param str model: Air condition model + :param str command: Command to execute + """ + + self_function = "{:02X}".format(121) + # FE + 0487 + 00007145 + 9470 + 1FFF7FFF + 06 + 0042 + 27 + 4E + 0025002D008500AC01... + command = code[0:2] + model[4:8] + model[8:16] + '9470' + \ + '1FFF' + self_function + 'FF' + \ + code[26:28] + code[28:32] + '27' + + checksum = sum([int(command[i:i + 2], 16) for i in range(0, len(command), 2)]) + checksum = "{:02X}".format(checksum % 256) + + command = command + checksum + code[36:] + + return self.send("send_ir_code", [command]) @command( click.argument("command", type=str), From 8cc6b07a16adcaea0a86cef4e233596caf490fd1 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 10 May 2018 18:39:56 +0200 Subject: [PATCH 02/14] Fix tests --- miio/tests/test_airconditioningcompanion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index caffba66e..d8db72c2f 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -131,7 +131,7 @@ def test_learn_stop(self): assert self.device.learn_stop() is True def test_send_ir_code(self): - assert self.device.send_ir_code('0000000') is True + assert self.device.send_ir_code('010500978022222102', '0000000') is True def test_send_command(self): assert self.device.send_command('0000000') is True From 5a94c6ac9f64697229086fc9ce588d9e622b0a91 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 10 May 2018 21:26:12 +0200 Subject: [PATCH 03/14] Make internal slot addressable --- miio/airconditioningcompanion.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index 7b43abc07..2effd136d 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -5,11 +5,15 @@ import click from .click_common import command, format_output, EnumType -from .device import Device +from .device import Device, DeviceException _LOGGER = logging.getLogger(__name__) +class AirConditioningCompanionException(DeviceException): + pass + + class OperationMode(enum.Enum): Heat = 0 Cool = 1 @@ -310,17 +314,21 @@ def learn_stop(self, slot: int=STORAGE_SLOT_ID): click.argument("code", type=str), default_output=format_output("Sending the supplied infrared command") ) - def send_ir_code(self, model: str, code: str): + def send_ir_code(self, model: str, code: str, slot: int=0): """Play a captured command. :param str model: Air condition model :param str command: Command to execute + :param int slot: Unknown internal register or slot """ + if slot < 0 or slot > 132: + raise AirConditioningCompanionException("Invalid slot: %s" % slot) + + slot = "{:02X}".format(121 + slot) - self_function = "{:02X}".format(121) - # FE + 0487 + 00007145 + 9470 + 1FFF7FFF + 06 + 0042 + 27 + 4E + 0025002D008500AC01... + # FE + 0487 + 00007145 + 9470 + 1FFF + 7F + FF + 06 + 0042 + 27 + 4E + 0025002D008500AC01... command = code[0:2] + model[4:8] + model[8:16] + '9470' + \ - '1FFF' + self_function + 'FF' + \ + '1FFF' + slot + 'FF' + \ code[26:28] + code[28:32] + '27' checksum = sum([int(command[i:i + 2], 16) for i in range(0, len(command), 2)]) From 273d1ebd3050356c50a2d824c60a1a8df5a998d9 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 10 May 2018 21:27:59 +0200 Subject: [PATCH 04/14] Use bit-wise operation vs. arithmetic one --- miio/airconditioningcompanion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index 2effd136d..5cb0bcd13 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -332,7 +332,7 @@ def send_ir_code(self, model: str, code: str, slot: int=0): code[26:28] + code[28:32] + '27' checksum = sum([int(command[i:i + 2], 16) for i in range(0, len(command), 2)]) - checksum = "{:02X}".format(checksum % 256) + checksum = "{:02X}".format(checksum & 0xFF) command = command + checksum + code[36:] From 8f547af3585bc61a373d856f68a89d58ef0edd23 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 10 May 2018 22:25:38 +0200 Subject: [PATCH 05/14] Use bytes for model and code --- miio/airconditioningcompanion.py | 40 ++++++++++----------- miio/tests/test_airconditioningcompanion.py | 4 +-- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index 5cb0bcd13..7524f4ee4 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -103,19 +103,19 @@ def load_power(self) -> int: return int(self.data[2]) @property - def air_condition_model(self) -> str: + def air_condition_model(self) -> bytes: """Model of the air conditioner.""" - return self.data[0] + return bytes.fromhex(self.data[0]) @property def model_format(self) -> int: """Version number of the model format.""" - return int(self.air_condition_model[0:2]) + return self.air_condition_model[0] @property def device_type(self) -> int: """Device type identifier.""" - return int(self.air_condition_model[2:4]) + return self.air_condition_model[1] @property def air_condition_brand(self) -> int: @@ -124,7 +124,7 @@ def air_condition_brand(self) -> int: Known brand ids (int) are 0182, 0097, 0037, 0202, 02782, 0197, 0192. """ - return int(self.air_condition_model[4:8]) + return int(self.air_condition_model[2:4].hex()) @property def air_condition_remote(self) -> int: @@ -140,7 +140,7 @@ def air_condition_remote(self) -> int: 80666661 (brand: 192) """ - return int(self.air_condition_model[8:16]) + return int(self.air_condition_model[4:8].hex()) @property def state_format(self) -> int: @@ -149,7 +149,7 @@ def state_format(self) -> int: Known values (int) are: 01, 02, 03 """ - return int(self.air_condition_model[16:18]) + return int(self.air_condition_model[8]) @property def air_condition_configuration(self) -> int: @@ -231,7 +231,7 @@ def __repr__(self) -> str: "mode=%s>" % \ (self.power, self.load_power, - self.air_condition_model, + self.air_condition_model.hex(), self.model_format, self.device_type, self.air_condition_brand, @@ -310,33 +310,29 @@ def learn_stop(self, slot: int=STORAGE_SLOT_ID): return self.send("end_ir_learn", [slot]) @command( - click.argument("model", type=str), - click.argument("code", type=str), + click.argument("model", type=bytes), + click.argument("code", type=bytes), default_output=format_output("Sending the supplied infrared command") ) - def send_ir_code(self, model: str, code: str, slot: int=0): + def send_ir_code(self, model: bytes, code: bytes, slot: int=0): """Play a captured command. - :param str model: Air condition model - :param str command: Command to execute + :param bytes model: Air condition model + :param bytes code: Command to execute :param int slot: Unknown internal register or slot """ if slot < 0 or slot > 132: raise AirConditioningCompanionException("Invalid slot: %s" % slot) - slot = "{:02X}".format(121 + slot) + slot = bytes([121 + slot]) # FE + 0487 + 00007145 + 9470 + 1FFF + 7F + FF + 06 + 0042 + 27 + 4E + 0025002D008500AC01... - command = code[0:2] + model[4:8] + model[8:16] + '9470' + \ - '1FFF' + slot + 'FF' + \ - code[26:28] + code[28:32] + '27' - - checksum = sum([int(command[i:i + 2], 16) for i in range(0, len(command), 2)]) - checksum = "{:02X}".format(checksum & 0xFF) + command = code[0:1] + model[2:8] + b'\x94\x70\x1F\xFF' + slot + b'\xFF' + code[13:16] + b'\x27' - command = command + checksum + code[36:] + checksum = sum(command) & 0xFF + command = command + bytes([checksum]) + code[18:] - return self.send("send_ir_code", [command]) + return self.send("send_ir_code", [command.hex().upper()]) @command( click.argument("command", type=str), diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index d8db72c2f..80be24e45 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -86,7 +86,7 @@ def test_status(self): assert self.is_on() is False assert self.state().load_power == 2 - assert self.state().air_condition_model == '010500978022222102' + assert self.state().air_condition_model == bytes.fromhex('010500978022222102') assert self.state().model_format == 1 assert self.state().device_type == 5 assert self.state().air_condition_brand == 97 @@ -131,7 +131,7 @@ def test_learn_stop(self): assert self.device.learn_stop() is True def test_send_ir_code(self): - assert self.device.send_ir_code('010500978022222102', '0000000') is True + assert self.device.send_ir_code(bytes.fromhex('010500978022222102'), bytes.fromhex('00')) is True def test_send_command(self): assert self.device.send_command('0000000') is True From 4cfcc653f1bc38331a3745135c176945eed55fb9 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 10 May 2018 22:28:16 +0200 Subject: [PATCH 06/14] Fix lint --- miio/airconditioningcompanion.py | 3 ++- miio/tests/test_airconditioningcompanion.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index 7524f4ee4..f6ace2896 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -327,7 +327,8 @@ def send_ir_code(self, model: bytes, code: bytes, slot: int=0): slot = bytes([121 + slot]) # FE + 0487 + 00007145 + 9470 + 1FFF + 7F + FF + 06 + 0042 + 27 + 4E + 0025002D008500AC01... - command = code[0:1] + model[2:8] + b'\x94\x70\x1F\xFF' + slot + b'\xFF' + code[13:16] + b'\x27' + command = code[0:1] + model[2:8] + b'\x94\x70\x1F\xFF' + \ + slot + b'\xFF' + code[13:16] + b'\x27' checksum = sum(command) & 0xFF command = command + bytes([checksum]) + code[18:] diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index 80be24e45..4d810d809 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -86,7 +86,8 @@ def test_status(self): assert self.is_on() is False assert self.state().load_power == 2 - assert self.state().air_condition_model == bytes.fromhex('010500978022222102') + assert self.state().air_condition_model == \ + bytes.fromhex('010500978022222102') assert self.state().model_format == 1 assert self.state().device_type == 5 assert self.state().air_condition_brand == 97 From 8d9e58411e1aaac7bbc7fc5d0faff39bc42140d1 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 10 May 2018 22:30:47 +0200 Subject: [PATCH 07/14] Fix lint --- miio/airconditioningcompanion.py | 2 +- miio/tests/test_airconditioningcompanion.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index f6ace2896..63ee924ae 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -328,7 +328,7 @@ def send_ir_code(self, model: bytes, code: bytes, slot: int=0): # FE + 0487 + 00007145 + 9470 + 1FFF + 7F + FF + 06 + 0042 + 27 + 4E + 0025002D008500AC01... command = code[0:1] + model[2:8] + b'\x94\x70\x1F\xFF' + \ - slot + b'\xFF' + code[13:16] + b'\x27' + slot + b'\xFF' + code[13:16] + b'\x27' checksum = sum(command) & 0xFF command = command + bytes([checksum]) + code[18:] diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index 4d810d809..014e33be5 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -87,7 +87,7 @@ def test_status(self): assert self.is_on() is False assert self.state().load_power == 2 assert self.state().air_condition_model == \ - bytes.fromhex('010500978022222102') + bytes.fromhex('010500978022222102') assert self.state().model_format == 1 assert self.state().device_type == 5 assert self.state().air_condition_brand == 97 @@ -132,7 +132,8 @@ def test_learn_stop(self): assert self.device.learn_stop() is True def test_send_ir_code(self): - assert self.device.send_ir_code(bytes.fromhex('010500978022222102'), bytes.fromhex('00')) is True + assert self.device.send_ir_code(bytes.fromhex('010500978022222102'), + bytes.fromhex('00')) is True def test_send_command(self): assert self.device.send_command('0000000') is True From c3ce22c8870ef055e500b614841f6c0e79fd5857 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 13 May 2018 15:01:50 +0200 Subject: [PATCH 08/14] Introduce new click parameter type for hexadecimal strings --- miio/airconditioningcompanion.py | 8 ++++---- miio/click_common.py | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index 63ee924ae..f1f2d94ee 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -4,7 +4,7 @@ import click -from .click_common import command, format_output, EnumType +from .click_common import command, format_output, EnumType, HexStringParamType from .device import Device, DeviceException _LOGGER = logging.getLogger(__name__) @@ -310,8 +310,8 @@ def learn_stop(self, slot: int=STORAGE_SLOT_ID): return self.send("end_ir_learn", [slot]) @command( - click.argument("model", type=bytes), - click.argument("code", type=bytes), + click.argument("model", type=HexStringParamType), + click.argument("code", type=HexStringParamType), default_output=format_output("Sending the supplied infrared command") ) def send_ir_code(self, model: bytes, code: bytes, slot: int=0): @@ -346,7 +346,7 @@ def send_command(self, command: str): return self.send("send_cmd", [str(command)]) @command( - click.argument("model", type=str), + click.argument("model", type=HexStringParamType), click.argument("power", type=EnumType(Power, False)), click.argument("operation_mode", type=EnumType(OperationMode, False)), click.argument("target_temperature", type=int), diff --git a/miio/click_common.py b/miio/click_common.py index 1de4c0f52..84834dcfd 100644 --- a/miio/click_common.py +++ b/miio/click_common.py @@ -93,6 +93,14 @@ def get_metavar(self, param): return ("_".join(word)).upper() +class HexStringParamType(click.ParamType): + def convert(self, value, param, ctx): + try: + return bytes.fromhex(value) + except ValueError: + self.fail('%s is not a valid hexadecimal string' % value, param, ctx) + + class GlobalContextObject: def __init__(self, debug: int=0, output: callable=None): self.debug = debug From 0dc19210d7759a391886b37158c82518f5539206 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 13 May 2018 15:36:19 +0200 Subject: [PATCH 09/14] Extend tests --- miio/airconditioningcompanion.py | 2 +- miio/tests/test_airconditioningcompanion.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index f1f2d94ee..6ec61ad0e 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -321,7 +321,7 @@ def send_ir_code(self, model: bytes, code: bytes, slot: int=0): :param bytes code: Command to execute :param int slot: Unknown internal register or slot """ - if slot < 0 or slot > 132: + if slot < 0 or slot > 134: raise AirConditioningCompanionException("Invalid slot: %s" % slot) slot = bytes([121 + slot]) diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index 014e33be5..8e1a9b6a9 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -7,6 +7,7 @@ from miio.airconditioningcompanion import (OperationMode, FanSpeed, Power, SwingMode, Led, AirConditioningCompanionStatus, + AirConditioningCompanionException, STORAGE_SLOT_ID, ) STATE_ON = ['on'] @@ -135,6 +136,14 @@ def test_send_ir_code(self): assert self.device.send_ir_code(bytes.fromhex('010500978022222102'), bytes.fromhex('00')) is True + with pytest.raises(AirConditioningCompanionException): + self.device.send_ir_code(bytes.fromhex('010500978022222102'), + bytes.fromhex('00'), -1) + + with pytest.raises(AirConditioningCompanionException): + self.device.send_ir_code(bytes.fromhex('010500978022222102'), + bytes.fromhex('00'), 1+255-121) + def test_send_command(self): assert self.device.send_command('0000000') is True From 3b74a0fb9d84e2c0a540f1d28a3e9c573bf28c8e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 13 May 2018 15:44:56 +0200 Subject: [PATCH 10/14] Make hound happy --- miio/tests/test_airconditioningcompanion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index 8e1a9b6a9..b397657c7 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -134,7 +134,7 @@ def test_learn_stop(self): def test_send_ir_code(self): assert self.device.send_ir_code(bytes.fromhex('010500978022222102'), - bytes.fromhex('00')) is True + bytes.fromhex('00')) is True with pytest.raises(AirConditioningCompanionException): self.device.send_ir_code(bytes.fromhex('010500978022222102'), From 7dc6e2730a5f27f471a836e9767b421bd10c295a Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 13 May 2018 17:29:51 +0200 Subject: [PATCH 11/14] Refactoring --- miio/airconditioningcompanion.py | 26 +++++--- miio/click_common.py | 8 --- miio/tests/test_airconditioningcompanion.json | 61 +++++++++++++++++++ miio/tests/test_airconditioningcompanion.py | 50 ++++++++++----- 4 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 miio/tests/test_airconditioningcompanion.json diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index 6ec61ad0e..a477812aa 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -4,7 +4,7 @@ import click -from .click_common import command, format_output, EnumType, HexStringParamType +from .click_common import command, format_output, EnumType from .device import Device, DeviceException _LOGGER = logging.getLogger(__name__) @@ -310,17 +310,29 @@ def learn_stop(self, slot: int=STORAGE_SLOT_ID): return self.send("end_ir_learn", [slot]) @command( - click.argument("model", type=HexStringParamType), - click.argument("code", type=HexStringParamType), + click.argument("model", type=str), + click.argument("code", type=str), default_output=format_output("Sending the supplied infrared command") ) - def send_ir_code(self, model: bytes, code: bytes, slot: int=0): + def send_ir_code(self, model: str, code: str, slot: int=0): """Play a captured command. - :param bytes model: Air condition model - :param bytes code: Command to execute + :param str model: Air condition model + :param str code: Command to execute :param int slot: Unknown internal register or slot """ + try: + model = bytes.fromhex(model) + except: + raise AirConditioningCompanionException( + "Invalid model. A hexadecimal string must be provided") + + try: + code = bytes.fromhex(code) + except: + raise AirConditioningCompanionException( + "Invalid code. A hexadecimal string must be provided") + if slot < 0 or slot > 134: raise AirConditioningCompanionException("Invalid slot: %s" % slot) @@ -346,7 +358,7 @@ def send_command(self, command: str): return self.send("send_cmd", [str(command)]) @command( - click.argument("model", type=HexStringParamType), + click.argument("model", type=str), click.argument("power", type=EnumType(Power, False)), click.argument("operation_mode", type=EnumType(OperationMode, False)), click.argument("target_temperature", type=int), diff --git a/miio/click_common.py b/miio/click_common.py index 84834dcfd..1de4c0f52 100644 --- a/miio/click_common.py +++ b/miio/click_common.py @@ -93,14 +93,6 @@ def get_metavar(self, param): return ("_".join(word)).upper() -class HexStringParamType(click.ParamType): - def convert(self, value, param, ctx): - try: - return bytes.fromhex(value) - except ValueError: - self.fail('%s is not a valid hexadecimal string' % value, param, ctx) - - class GlobalContextObject: def __init__(self, debug: int=0, output: callable=None): self.debug = debug diff --git a/miio/tests/test_airconditioningcompanion.json b/miio/tests/test_airconditioningcompanion.json new file mode 100644 index 000000000..07f73fe44 --- /dev/null +++ b/miio/tests/test_airconditioningcompanion.json @@ -0,0 +1,61 @@ +{ + "test_raw_ok": [ + { + "in": [ + "010504870000714501", + "FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" + ], + "out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" + }, + { + "in": [ + "010504870000714501", + "FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517", + 1 + ], + "out": "FE04870000714594701FFF7AFF06004227490025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" + }, + { + "in": [ + "010504870000714501", + "FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517", + 134 + ], + "out": "FE04870000714594701FFFFFFF06004227CE0025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" + } + ], + "test_raw_exception": [ + { + "in": [ + "010504870000714501", + "FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517", + -1 + ], + "out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" + }, + { + "in": [ + "010504870000714501", + "FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517", + 135 + ], + "out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" + }, + { + "in": [ + "Y", + "FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517", + 0 + ], + "out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" + }, + { + "in": [ + "010504870000714501", + "Z", + 0 + ], + "out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" + } + ] +} \ No newline at end of file diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index b397657c7..15bc06f6d 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -1,4 +1,7 @@ +import base64 import string +import json +import os from unittest import TestCase import pytest @@ -10,20 +13,26 @@ AirConditioningCompanionException, STORAGE_SLOT_ID, ) +with open(os.path.join(os.path.dirname(__file__), + 'test_airconditioningcompanion.json')) as inp: + test_data = json.load(inp) + STATE_ON = ['on'] STATE_OFF = ['off'] + class DummyAirConditioningCompanion(AirConditioningCompanion): def __init__(self, *args, **kwargs): self.state = ['010500978022222102', '01020119A280222221', '2'] + self.last_ir_played = None self.return_values = { 'get_model_and_state': self._get_state, 'start_ir_learn': lambda x: True, 'end_ir_learn': lambda x: True, 'get_ir_learn_result': lambda x: True, - 'send_ir_code': lambda x: True, - 'send_cmd': self._send_cmd_input_validation, + 'send_ir_code': lambda x: self._send_ir_code_input_validation(x), + 'send_cmd': self._hex_input_validation, 'set_power': lambda x: self._set_power(x), } self.start_state = self.state.copy() @@ -48,8 +57,19 @@ def _set_power(self, value: str): if value == STATE_OFF: self.state[1] = self.state[1][:2] + '0' + self.state[1][3:] - def _send_cmd_input_validation(self, props): - return all(c in string.hexdigits for c in props[0]) + @staticmethod + def _hex_input_validation(payload): + return all(c in string.hexdigits for c in payload[0]) + + def _send_ir_code_input_validation(self, payload): + if self._hex_input_validation(payload[0]): + self.last_ir_played = payload[0] + return True + + return False + + def get_last_ir_played(self): + return self.last_ir_played @pytest.fixture(scope="class") @@ -133,16 +153,18 @@ def test_learn_stop(self): assert self.device.learn_stop() is True def test_send_ir_code(self): - assert self.device.send_ir_code(bytes.fromhex('010500978022222102'), - bytes.fromhex('00')) is True - - with pytest.raises(AirConditioningCompanionException): - self.device.send_ir_code(bytes.fromhex('010500978022222102'), - bytes.fromhex('00'), -1) - - with pytest.raises(AirConditioningCompanionException): - self.device.send_ir_code(bytes.fromhex('010500978022222102'), - bytes.fromhex('00'), 1+255-121) + for args in test_data['test_raw_ok']: + with self.subTest(): + self.device._reset_state() + self.assertTrue(self.device.send_ir_code(*args['in'])) + self.assertSequenceEqual( + self.device.get_last_ir_played(), + args['out'] + ) + + for args in test_data['test_raw_exception']: + with pytest.raises(AirConditioningCompanionException): + self.device.send_ir_code(*args['in']) def test_send_command(self): assert self.device.send_command('0000000') is True From d8cc6efa533a048ab4b4887e8c50b7b66f69eb49 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 13 May 2018 17:32:33 +0200 Subject: [PATCH 12/14] Do not use bare except --- miio/airconditioningcompanion.py | 4 ++-- miio/tests/test_airconditioningcompanion.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index a477812aa..7364b340b 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -323,13 +323,13 @@ def send_ir_code(self, model: str, code: str, slot: int=0): """ try: model = bytes.fromhex(model) - except: + except ValueError: raise AirConditioningCompanionException( "Invalid model. A hexadecimal string must be provided") try: code = bytes.fromhex(code) - except: + except ValueError: raise AirConditioningCompanionException( "Invalid code. A hexadecimal string must be provided") diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index 15bc06f6d..530a8a9f3 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -1,4 +1,3 @@ -import base64 import string import json import os From a23425d8a6be459e0659ba327875b3f90fa6dfe0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 13 May 2018 18:01:35 +0200 Subject: [PATCH 13/14] Enhance tests --- miio/tests/test_airconditioningcompanion.json | 42 +++++++++- miio/tests/test_airconditioningcompanion.py | 81 +++++++++---------- 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/miio/tests/test_airconditioningcompanion.json b/miio/tests/test_airconditioningcompanion.json index 07f73fe44..f66c56710 100644 --- a/miio/tests/test_airconditioningcompanion.json +++ b/miio/tests/test_airconditioningcompanion.json @@ -1,5 +1,5 @@ { - "test_raw_ok": [ + "test_send_ir_code_ok": [ { "in": [ "010504870000714501", @@ -24,7 +24,7 @@ "out": "FE04870000714594701FFFFFFF06004227CE0025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" } ], - "test_raw_exception": [ + "test_send_ir_code_exception": [ { "in": [ "010504870000714501", @@ -57,5 +57,43 @@ ], "out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517" } + ], + "test_send_configuration_ok": [ + { + "in": ["010000000001072700", {"__enum__": "Power.Off"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}], + "out": "010001072701011101004000205002112000D04000207002000000A0" + }, + { + "in": ["010000000001072700", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}], + "out": "010001072712001611001906205002102000C0190620700200000090" + }, + { + "in": ["010000000001072700", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.High"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}], + "out": "010001072712201611001906205002102000C0190620700200000090" + }, + { + "in": ["010000000001072700", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.Off"}, {"__enum__": "Led.Off"}], + "out": "010001072712011611001906205002102000C0190620700200000090" + }, + { + "in": ["010000000001072700", {"__enum__": "Power.Off"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.On"}], + "out": "010001072701011101004000205002112000D04000207002000000A0" + }, + { + "in": ["010000000001072700", {"__enum__": "Power.Off"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.Off"}, {"__enum__": "Led.Off"}], + "out": "010001072701011101004000205002112000D04000207002000000A0" + }, + { + "in": ["010000000001072700", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 23, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}], + "out": "010001072712001711001907205002102000D01907207002000000A0" + }, + { + "in": ["010000000001072700", {"__enum__": "Power.Off"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}], + "out": "010001072701011101004000205002112000D04000207002000000A0" + }, + { + "in": ["010507950000257301", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}], + "out": "0100002573120016A1" + } ] } \ No newline at end of file diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index 530a8a9f3..131d9bcf9 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -12,12 +12,35 @@ AirConditioningCompanionException, STORAGE_SLOT_ID, ) +STATE_ON = ['on'] +STATE_OFF = ['off'] + +PUBLIC_ENUMS = { + 'OperationMode': OperationMode, + 'FanSpeed': FanSpeed, + 'Power': Power, + 'SwingMode': SwingMode, + 'Led': Led, +} + + +def as_enum(d): + if "__enum__" in d: + name, member = d["__enum__"].split(".") + return getattr(PUBLIC_ENUMS[name], member) + else: + return d + with open(os.path.join(os.path.dirname(__file__), 'test_airconditioningcompanion.json')) as inp: - test_data = json.load(inp) + test_data = json.load(inp, object_hook=as_enum) -STATE_ON = ['on'] -STATE_OFF = ['off'] + +class EnumEncoder(json.JSONEncoder): + def default(self, obj): + if type(obj) in PUBLIC_ENUMS.values(): + return {"__enum__": str(obj)} + return json.JSONEncoder.default(self, obj) class DummyAirConditioningCompanion(AirConditioningCompanion): @@ -30,8 +53,8 @@ def __init__(self, *args, **kwargs): 'start_ir_learn': lambda x: True, 'end_ir_learn': lambda x: True, 'get_ir_learn_result': lambda x: True, - 'send_ir_code': lambda x: self._send_ir_code_input_validation(x), - 'send_cmd': self._hex_input_validation, + 'send_ir_code': lambda x: self._send_input_validation(x), + 'send_cmd': lambda x: self._send_input_validation(x), 'set_power': lambda x: self._set_power(x), } self.start_state = self.state.copy() @@ -60,7 +83,7 @@ def _set_power(self, value: str): def _hex_input_validation(payload): return all(c in string.hexdigits for c in payload[0]) - def _send_ir_code_input_validation(self, payload): + def _send_input_validation(self, payload): if self._hex_input_validation(payload[0]): self.last_ir_played = payload[0] return True @@ -152,7 +175,7 @@ def test_learn_stop(self): assert self.device.learn_stop() is True def test_send_ir_code(self): - for args in test_data['test_raw_ok']: + for args in test_data['test_send_ir_code_ok']: with self.subTest(): self.device._reset_state() self.assertTrue(self.device.send_ir_code(*args['in'])) @@ -161,7 +184,7 @@ def test_send_ir_code(self): args['out'] ) - for args in test_data['test_raw_exception']: + for args in test_data['test_send_ir_code_exception']: with pytest.raises(AirConditioningCompanionException): self.device.send_ir_code(*args['in']) @@ -169,36 +192,12 @@ def test_send_command(self): assert self.device.send_command('0000000') is True def test_send_configuration(self): - def send_configuration_known_aircondition(): - return self.device.send_configuration( - '010000000001072700', # best guess - Power.On, - OperationMode.Auto, - 22, - FanSpeed.Low, - SwingMode.On, - Led.Off) - - def send_configuration_known_aircondition_turn_off(): - return self.device.send_configuration( - '010000000001072700', # best guess - Power.Off, - OperationMode.Auto, - 22, - FanSpeed.Low, - SwingMode.On, - Led.Off) - - def send_configuration_unknown_aircondition(): - return self.device.send_configuration( - '010507950000257301', - Power.On, - OperationMode.Auto, - 22, - FanSpeed.Low, - SwingMode.On, - Led.Off) - - assert send_configuration_known_aircondition() is True - assert send_configuration_known_aircondition_turn_off() is True - assert send_configuration_unknown_aircondition() is True + + for args in test_data['test_send_configuration_ok']: + with self.subTest(): + self.device._reset_state() + self.assertTrue(self.device.send_configuration(*args['in'])) + self.assertSequenceEqual( + self.device.get_last_ir_played(), + args['out'] + ) From 8b13cb8b6ad38c5ef3541c266864d667e15c790a Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 13 May 2018 18:03:03 +0200 Subject: [PATCH 14/14] Add blank line --- miio/tests/test_airconditioningcompanion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index 131d9bcf9..eea8cdbd8 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -31,6 +31,7 @@ def as_enum(d): else: return d + with open(os.path.join(os.path.dirname(__file__), 'test_airconditioningcompanion.json')) as inp: test_data = json.load(inp, object_hook=as_enum)