-
-
Notifications
You must be signed in to change notification settings - Fork 568
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
Air Conditioning Companion: Rewrite a captured command before replay #317
Changes from 11 commits
f2c749a
8cc6b07
5a94c6a
273d1eb
8f547af
4cfcc65
8d9e584
c3ce22c
0dc1921
3b74a0f
7dc6e27
d8cc6ef
a23425d
8b13cb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -99,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: | ||
|
@@ -120,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: | ||
|
@@ -136,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: | ||
|
@@ -145,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: | ||
|
@@ -227,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, | ||
|
@@ -306,14 +310,42 @@ 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, slot: int=0): | ||
"""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 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do not use bare except' |
||
raise AirConditioningCompanionException( | ||
"Invalid code. A hexadecimal string must be provided") | ||
|
||
if slot < 0 or slot > 134: | ||
raise AirConditioningCompanionException("Invalid slot: %s" % slot) | ||
|
||
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' | ||
|
||
checksum = sum(command) & 0xFF | ||
command = command + bytes([checksum]) + code[18:] | ||
|
||
return self.send("send_ir_code", [command.hex().upper()]) | ||
|
||
@command( | ||
click.argument("command", type=str), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
import base64 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 'base64' imported but unused |
||
import string | ||
import json | ||
import os | ||
from unittest import TestCase | ||
|
||
import pytest | ||
|
@@ -7,22 +10,29 @@ | |
from miio.airconditioningcompanion import (OperationMode, FanSpeed, Power, | ||
SwingMode, Led, | ||
AirConditioningCompanionStatus, | ||
AirConditioningCompanionException, | ||
STORAGE_SLOT_ID, ) | ||
|
||
with open(os.path.join(os.path.dirname(__file__), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. expected 2 blank lines after class or function definition, found 1 |
||
'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() | ||
|
@@ -47,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") | ||
|
@@ -86,7 +107,8 @@ 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 +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('0000000') is True | ||
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do not use bare except'