Skip to content

Commit

Permalink
add support airconditioningcompanion KTBL03LM
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeneLiu committed Sep 2, 2020
1 parent 93b7a77 commit 69174dd
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 1 deletion.
1 change: 1 addition & 0 deletions miio/__init__.py
Expand Up @@ -4,6 +4,7 @@
AirConditioningCompanion,
AirConditioningCompanionV3,
)
from miio.airconditioningcompanionMCN import AirConditioningCompanionMcn02
from miio.airdehumidifier import AirDehumidifier
from miio.airfresh import AirFresh, AirFreshVA4
from miio.airfresh_t2017 import AirFreshT2017
Expand Down
179 changes: 179 additions & 0 deletions miio/airconditioningcompanionMCN.py
@@ -0,0 +1,179 @@
import enum
import logging
import random
from typing import Any, Optional

from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException

_LOGGER = logging.getLogger(__name__)

MODEL_ACPARTNER_MCN02 = "lumi.acpartner.mcn02"


class AirConditioningCompanionException(DeviceException):
pass


class OperationMode(enum.Enum):
Cool = "cool"
Heat = "heat"
Auto = "auto"
Ventilate = "wind"
Dehumidify = "dry"


class FanSpeed(enum.Enum):
Auto = "auto_fan"
Low = "small_fan"
Medium = "medium_fan"
High = "large_fan"


class SwingMode(enum.Enum):
On = "on"
Off = "off"


class AirConditioningCompanionStatus:
"""Container for status reports of the Xiaomi AC Companion."""

def __init__(self, data):
"""
Device model: lumi.acpartner.mcn02
Response of "get_prop, params:['power', 'mode', 'tar_temp', 'fan_level', 'ver_swing', 'load_power']":
['on', 'dry', 16, 'small_fan', 'off', 84.0]
"""
self.data = data

@property
def load_power(self) -> int:
"""Current power load of the air conditioner."""
return int(self.data[-1])

@property
def power(self) -> str:
"""Current power state."""
return self.data[0]

@property
def is_on(self) -> bool:
"""True if the device is turned on."""
return self.power == "on"

@property
def mode(self) -> Optional[OperationMode]:
"""Current operation mode."""
try:
mode = self.data[1]
return OperationMode(mode)
except TypeError:
return None

@property
def target_temperature(self) -> Optional[int]:
"""Target temperature."""
try:
return self.data[2]
except TypeError:
return None

@property
def fan_speed(self) -> Optional[FanSpeed]:
"""Current fan speed."""
try:
speed = self.data[3]
return FanSpeed(speed)
except TypeError:
return None

@property
def swing_mode(self) -> Optional[SwingMode]:
"""Current swing mode."""
try:
mode = self.data[4]
return SwingMode(mode)
except TypeError:
return None

def __repr__(self) -> str:
s = (
"<AirConditioningCompanionStatus "
"power=%s, "
"load_power=%s, "
"target_temperature=%s, "
"swing_mode=%s, "
"fan_speed=%s, "
"mode=%s>"
% (
self.power,
self.load_power,
self.target_temperature,
self.swing_mode,
self.fan_speed,
self.mode,
)
)
return s

def __json__(self):
return self.data


class AirConditioningCompanionMcn02(Device):
"""Main class representing Xiaomi Air Conditioning Companion V1 and V2."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = random.randint(0, 999),
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_ACPARTNER_MCN02,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)

if model != MODEL_ACPARTNER_MCN02:
_LOGGER.error(
"Device model %s unsupported. Please use AirConditioningCompanion",
model,
)

@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Load power: {result.load_power}\n"
"Target temperature: {result.target_temperature} °C\n"
"Swing mode: {result.swing_mode}\n"
"Fan speed: {result.fan_speed}\n"
"Mode: {result.mode}\n",
)
)
def status(self) -> AirConditioningCompanionStatus:
"""Return device status."""
data = self.send(
"get_prop",
["power", "mode", "tar_temp", "fan_level", "ver_swing", "load_power"],
)
return AirConditioningCompanionStatus(data)

@command(default_output=format_output("Powering the air condition on"))
def on(self):
"""Turn the air condition on by infrared."""
return self.send("set_power", ["on"])

@command(default_output=format_output("Powering the air condition off"))
def off(self):
"""Turn the air condition off by infrared."""
return self.send("set_power", ["off"])

@command(default_output=format_output("Sending a command to the air conditioner"),)
def send_command(self, command: str, parameters: Any = None) -> Any:
"""Send a command to the air conditioner.
:param str command: Command to execute"""
return self.send(command, parameters)
5 changes: 5 additions & 0 deletions miio/discovery.py
Expand Up @@ -9,6 +9,7 @@

from . import (
AirConditioningCompanion,
AirConditioningCompanionMcn02,
AirFresh,
AirFreshT2017,
AirHumidifier,
Expand Down Expand Up @@ -45,6 +46,7 @@
MODEL_ACPARTNER_V2,
MODEL_ACPARTNER_V3,
)
from .airconditioningcompanionMCN import MODEL_ACPARTNER_MCN02
from .airfresh import MODEL_AIRFRESH_VA2, MODEL_AIRFRESH_VA4
from .airhumidifier import (
MODEL_HUMIDIFIER_CA1,
Expand Down Expand Up @@ -147,6 +149,9 @@
"lumi-acpartner-v1": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V1),
"lumi-acpartner-v2": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V2),
"lumi-acpartner-v3": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V3),
"lumi-acpartner-mcn02": partial(
AirConditioningCompanionMcn02, model=MODEL_ACPARTNER_MCN02
),
"lumi-camera-aq2": AqaraCamera,
"yeelink-light-": Yeelight,
"zhimi-fan-v2": partial(Fan, model=MODEL_FAN_V2),
Expand Down
59 changes: 58 additions & 1 deletion miio/tests/test_airconditioningcompanion.py
Expand Up @@ -5,7 +5,11 @@

import pytest

from miio import AirConditioningCompanion, AirConditioningCompanionV3
from miio import (
AirConditioningCompanion,
AirConditioningCompanionMcn02,
AirConditioningCompanionV3,
)
from miio.airconditioningcompanion import (
MODEL_ACPARTNER_V3,
STORAGE_SLOT_ID,
Expand All @@ -17,6 +21,13 @@
Power,
SwingMode,
)
from miio.airconditioningcompanionMCN import MODEL_ACPARTNER_MCN02
from miio.airconditioningcompanionMCN import (
AirConditioningCompanionStatus as AirConditioningCompanionStatusMcn02,
)
from miio.airconditioningcompanionMCN import FanSpeed as FanSpeedMcn02
from miio.airconditioningcompanionMCN import OperationMode as OperationModeMcn02
from miio.airconditioningcompanionMCN import SwingMode as SwingModeMcn02
from miio.tests.dummies import DummyDevice

STATE_ON = ["on"]
Expand Down Expand Up @@ -297,3 +308,49 @@ def test_status(self):
assert self.state().fan_speed == FanSpeed.Low
assert self.state().mode == OperationMode.Heat
assert self.state().led is True


class DummyAirConditioningCompanionMcn02(DummyDevice, AirConditioningCompanionMcn02):
def __init__(self, *args, **kwargs):
self.state = ["on", "cool", 28, "small_fan", "on", 441.0]
self.model = MODEL_ACPARTNER_MCN02

self.return_values = {"get_prop": self._get_state}
self.start_state = self.state.copy()
super().__init__(args, kwargs)

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 airconditioningcompanionMcn02(request):
request.cls.device = DummyAirConditioningCompanionMcn02()
# TODO add ability to test on a real device


@pytest.mark.usefixtures("airconditioningcompanionMcn02")
class TestAirConditioningCompanionMcn02(TestCase):
def state(self):
return self.device.status()

def is_on(self):
return self.device.status().is_on

def test_status(self):
self.device._reset_state()

assert repr(self.state()) == repr(
AirConditioningCompanionStatusMcn02(self.device.start_state)
)

assert self.is_on() is True
assert self.state().target_temperature == 28
assert self.state().swing_mode == SwingModeMcn02.On
assert self.state().fan_speed == FanSpeedMcn02.Low
assert self.state().mode == OperationModeMcn02.Cool

0 comments on commit 69174dd

Please sign in to comment.