-
-
Notifications
You must be signed in to change notification settings - Fork 563
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Xiaomi Air Conditioner Companion support (#129)
* First draft of the Xiaomi Mi Home Air Conditioner Companion supported. Fixes #76. * Unused imports removed. * Unit tests added. Example response added. Refactoring. * Unneeded method removed. * Method naming improved. * Naming improved. * Unit tests fixed. * Unused import removed.
- Loading branch information
Showing
3 changed files
with
164 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
from .device import Device | ||
import enum | ||
from typing import Optional | ||
|
||
|
||
class OperationMode(enum.Enum): | ||
Heating = 0 | ||
Cooling = 1 | ||
Auto = 2 | ||
|
||
|
||
class FanSpeed(enum.Enum): | ||
Low = 0 | ||
Medium = 1 | ||
High = 2 | ||
Auto = 3 | ||
|
||
|
||
STORAGE_SLOT_ID = 30 | ||
|
||
|
||
class AirConditioningCompanionStatus: | ||
"""Container for status reports of the Xiaomi AC Companion.""" | ||
|
||
def __init__(self, data): | ||
# Device model: lumi.acpartner.v2 | ||
# | ||
# Response of "get_model_and_state": | ||
# ['010500978022222102', '010201190280222221', '2'] | ||
self.data = data | ||
|
||
@property | ||
def air_condition_power(self) -> str: | ||
"""Current power state of the air conditioner.""" | ||
return str(self.data[2]) | ||
|
||
@property | ||
def air_condition_model(self) -> str: | ||
"""Model of the air conditioner.""" | ||
return str(self.data[0][0:2] + self.data[0][8:16]) | ||
|
||
@property | ||
def power(self) -> str: | ||
"""Current power state.""" | ||
return 'on' if (self.data[1][2:3] == 1) else 'off' | ||
|
||
@property | ||
def is_on(self) -> bool: | ||
"""True if the device is turned on.""" | ||
return self.power == 'on' | ||
|
||
@property | ||
def temperature(self) -> int: | ||
"""Current temperature.""" | ||
return int(self.data[1][6:8], 16) | ||
|
||
@property | ||
def swing_mode(self) -> bool: | ||
"""True if swing mode is enabled.""" | ||
return self.data[1][5:6] == '0' | ||
|
||
@property | ||
def fan_speed(self) -> Optional[FanSpeed]: | ||
"""Current fan speed.""" | ||
speed = int(self.data[1][4:5]) | ||
if speed is not None: | ||
return FanSpeed(speed) | ||
|
||
return None | ||
|
||
@property | ||
def mode(self) -> Optional[OperationMode]: | ||
"""Current operation mode.""" | ||
mode = int(self.data[1][3:4]) | ||
if mode is not None: | ||
return OperationMode(mode) | ||
|
||
return None | ||
|
||
|
||
class AirConditioningCompanion(Device): | ||
"""Main class representing Xiaomi Air Conditioning Companion.""" | ||
|
||
def status(self) -> AirConditioningCompanionStatus: | ||
"""Return device status.""" | ||
status = self.send("get_model_and_state", []) | ||
return AirConditioningCompanionStatus(status) | ||
|
||
def learn(self): | ||
"""Learn an infrared command.""" | ||
return self.send("start_ir_learn", [STORAGE_SLOT_ID]) | ||
|
||
def learn_result(self): | ||
"""Read the learned command.""" | ||
return self.send("get_ir_learn_result", []) | ||
|
||
def learn_stop(self): | ||
"""Stop learning of a infrared command.""" | ||
return self.send("end_ir_learn", [STORAGE_SLOT_ID]) | ||
|
||
def send_ir_code(self, command: str): | ||
"""Play a captured command. | ||
:param str command: Command to execute""" | ||
return self.send("send_ir_code", [str(command)]) | ||
|
||
def send_command(self, command: str): | ||
"""Send a command to the air conditioner. | ||
:param str command: Command to execute""" | ||
return self.send("send_cmd", [str(command)]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from unittest import TestCase | ||
from miio import AirConditioningCompanion | ||
from miio.airconditioningcompanion import OperationMode, FanSpeed | ||
import pytest | ||
|
||
|
||
class DummyAirConditioningCompanion(AirConditioningCompanion): | ||
def __init__(self, *args, **kwargs): | ||
self.state = ['010500978022222102', '010201190280222221', '2'] | ||
|
||
self.return_values = { | ||
'get_model_and_state': self._get_state | ||
} | ||
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 _get_state(self, props): | ||
"""Return the requested data""" | ||
return self.state | ||
|
||
|
||
@pytest.fixture(scope="class") | ||
def airconditioningcompanion(request): | ||
request.cls.device = DummyAirConditioningCompanion() | ||
# TODO add ability to test on a real device | ||
|
||
|
||
@pytest.mark.usefixtures("airconditioningcompanion") | ||
class TestAirConditioningCompanion(TestCase): | ||
def is_on(self): | ||
return self.device.status().is_on | ||
|
||
def state(self): | ||
return self.device.status() | ||
|
||
def test_status(self): | ||
self.device._reset_state() | ||
|
||
assert self.is_on() is False | ||
assert self.state().air_condition_power == '2' | ||
assert self.state().air_condition_model == '0180222221' | ||
assert self.state().temperature == 25 | ||
assert self.state().swing_mode is False | ||
assert self.state().fan_speed == FanSpeed.Low | ||
assert self.state().mode == OperationMode.Auto |