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

Add support for lumi.acpartner.mcn02 #809

Merged
merged 1 commit into from Sep 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Expand Up @@ -9,7 +9,7 @@ repos:
- id: debug-statements
- id: check-ast

- repo: https://github.com/ambv/black
- repo: https://github.com/psf/black
rev: stable
hooks:
- id: black
Expand Down
2 changes: 1 addition & 1 deletion docs/api/miio.rst
Expand Up @@ -8,6 +8,7 @@ Submodules
:maxdepth: 4

miio.airconditioningcompanion
miio.airconditioningcompanionMCN
miio.airdehumidifier
miio.airfilter_util
miio.airfresh
Expand Down Expand Up @@ -38,7 +39,6 @@ Submodules
miio.heater
miio.miioprotocol
miio.miot_device
miio.parse_ast
miio.philips_bulb
miio.philips_eyecare
miio.philips_eyecare_cli
Expand Down
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
181 changes: 181 additions & 0 deletions miio/airconditioningcompanionMCN.py
@@ -0,0 +1,181 @@
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