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

Air Conditioning Companion: Rewrite a captured command before replay #317

Merged
58 changes: 45 additions & 13 deletions miio/airconditioningcompanion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:

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'

raise AirConditioningCompanionException(
"Invalid model. A hexadecimal string must be provided")

try:
code = bytes.fromhex(code)
except:

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'

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),
Expand Down
61 changes: 61 additions & 0 deletions miio/tests/test_airconditioningcompanion.json
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"
}
]
}
45 changes: 39 additions & 6 deletions miio/tests/test_airconditioningcompanion.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import base64

Choose a reason for hiding this comment

The 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
Expand All @@ -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__),

Choose a reason for hiding this comment

The 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()
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down