Skip to content

Commit

Permalink
Xiaomi Air Conditioner Companion support (#129)
Browse files Browse the repository at this point in the history
* 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
syssi authored and rytilahti committed Nov 28, 2017
1 parent 81451d2 commit 151ec30
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 0 deletions.
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from miio.fan import Fan
from miio.wifispeaker import WifiSpeaker
from miio.airqualitymonitor import AirQualityMonitor
from miio.airconditioningcompanion import AirConditioningCompanion
from miio.yeelight import Yeelight
from miio.device import Device, DeviceException
from miio.discovery import Discovery
111 changes: 111 additions & 0 deletions miio/airconditioningcompanion.py
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)])
52 changes: 52 additions & 0 deletions miio/tests/test_airconditioningcompanion.py
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

0 comments on commit 151ec30

Please sign in to comment.