Skip to content

Commit

Permalink
Improve the support of the Air Humidifier CA1 (Closes: #383) (#388)
Browse files Browse the repository at this point in the history
  • Loading branch information
syssi authored Oct 8, 2018
1 parent e3da229 commit 8569e75
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 30 deletions.
73 changes: 57 additions & 16 deletions miio/airhumidifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@

_LOGGER = logging.getLogger(__name__)

MODEL_HUMIDIFIER_V1 = 'zhimi.humidifier.v1'
MODEL_HUMIDIFIER_CA1 = 'zhimi.humidifier.ca1'

AVAILABLE_PROPERTIES_COMMON = [
'power',
'mode',
'temp_dec',
'humidity',
'buzzer',
'led_b',
'child_lock',
'limit_hum',
'use_time',
'hw_version',
]

AVAILABLE_PROPERTIES = {
MODEL_HUMIDIFIER_V1: AVAILABLE_PROPERTIES_COMMON + ['trans_level', 'button_pressed'],
MODEL_HUMIDIFIER_CA1: AVAILABLE_PROPERTIES_COMMON + ['speed', 'depth', 'dry'],
}


class AirHumidifierException(DeviceException):
pass
Expand Down Expand Up @@ -96,13 +117,15 @@ def target_humidity(self) -> int:
return self.data["limit_hum"]

@property
def trans_level(self) -> int:
def trans_level(self) -> Optional[int]:
"""
The meaning of the property is unknown.
The property is used to determine the strong mode is enabled on old firmware.
"""
return self.data["trans_level"]
if "trans_level" in self.data and self.data["trans_level"] is not None:
return self.data["trans_level"]
return None

@property
def strong_mode_enabled(self) -> bool:
Expand Down Expand Up @@ -133,12 +156,16 @@ def firmware_version_minor(self) -> int:
@property
def speed(self) -> Optional[int]:
"""Current fan speed."""
return self.data["speed"]
if "speed" in self.data and self.data["speed"] is not None:
return self.data["speed"]
return None

@property
def depth(self) -> Optional[int]:
"""The remaining amount of water in percent."""
return self.data["depth"]
if "depth" in self.data and self.data["depth"] is not None:
return self.data["depth"]
return None

@property
def dry(self) -> Optional[bool]:
Expand All @@ -147,7 +174,7 @@ def dry(self) -> Optional[bool]:
Return True if dry mode is on if available.
"""
if self.data["dry"] is not None:
if "dry" in self.data and self.data["dry"] is not None:
return self.data["dry"] == "on"
return None

Expand All @@ -164,7 +191,9 @@ def hardware_version(self) -> Optional[str]:
@property
def button_pressed(self) -> Optional[str]:
"""Last pressed button."""
return self.data["button_pressed"]
if "button_pressed" in self.data and self.data["button_pressed"] is not None:
return self.data["button_pressed"]
return None

def __repr__(self) -> str:
s = "<AirHumidiferStatus power=%s, " \
Expand Down Expand Up @@ -213,9 +242,15 @@ class AirHumidifier(Device):
"""Implementation of Xiaomi Mi Air Humidifier."""

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

if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_HUMIDIFIER_V1

self.device_info = None

@command(
Expand Down Expand Up @@ -244,20 +279,26 @@ def status(self) -> AirHumidifierStatus:
if self.device_info is None:
self.device_info = self.info()

properties = ['power', 'mode', 'temp_dec', 'humidity', 'buzzer',
'led_b', 'child_lock', 'limit_hum', 'trans_level',
'speed', 'depth', 'dry', 'use_time', 'button_pressed',
'hw_version', ]
properties = AVAILABLE_PROPERTIES[self.model]

values = self.send(
"get_prop",
properties
)
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props_per_request = 15

# The CA1 is limited to a single property per request
if self.model == MODEL_HUMIDIFIER_CA1:
_props_per_request = 1

_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:_props_per_request]))
_props[:] = _props[_props_per_request:]

properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count, values_count)
Expand Down
215 changes: 201 additions & 14 deletions miio/tests/test_airhumidifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

from miio import AirHumidifier
from miio.airhumidifier import (OperationMode, LedBrightness,
AirHumidifierStatus, AirHumidifierException, )
AirHumidifierStatus, AirHumidifierException,
MODEL_HUMIDIFIER_V1, MODEL_HUMIDIFIER_CA1)
from .dummies import DummyDevice
from miio.device import DeviceInfo


class DummyAirHumidifier(DummyDevice, AirHumidifier):
class DummyAirHumidifierV1(DummyDevice, AirHumidifier):
def __init__(self, *args, **kwargs):
self.model = MODEL_HUMIDIFIER_V1
self.dummy_device_info = {
'fw_ver': '1.2.9_5033',
'token': '68ffffffffffffffffffffffffffffff',
Expand Down Expand Up @@ -45,6 +47,197 @@ def __init__(self, *args, **kwargs):
'use_time': 941100,
'button_pressed': 'led',
'hw_version': 0,
}
self.return_values = {
'get_prop': self._get_state,
'set_power': lambda x: self._set_state("power", x),
'set_mode': lambda x: self._set_state("mode", x),
'set_led_b': lambda x: self._set_state("led_b", x),
'set_buzzer': lambda x: self._set_state("buzzer", x),
'set_child_lock': lambda x: self._set_state("child_lock", x),
'set_limit_hum': lambda x: self._set_state("limit_hum", x),
'miIO.info': self._get_device_info,
}
super().__init__(args, kwargs)

def _get_device_info(self, _):
"""Return dummy device info."""
return self.dummy_device_info


@pytest.fixture(scope="class")
def airhumidifierv1(request):
request.cls.device = DummyAirHumidifierV1()
# TODO add ability to test on a real device


@pytest.mark.usefixtures("airhumidifierv1")
class TestAirHumidifierV1(TestCase):
def is_on(self):
return self.device.status().is_on

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

def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False

self.device.on()
assert self.is_on() is True

def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True

self.device.off()
assert self.is_on() is False

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

device_info = DeviceInfo(self.device.dummy_device_info)

assert repr(self.state()) == repr(AirHumidifierStatus(self.device.start_state, device_info))

assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temp_dec"] / 10.0
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().led_brightness == LedBrightness(self.device.start_state["led_b"])
assert self.state().buzzer == (self.device.start_state["buzzer"] == 'on')
assert self.state().child_lock == (self.device.start_state["child_lock"] == 'on')
assert self.state().target_humidity == self.device.start_state["limit_hum"]
assert self.state().trans_level == self.device.start_state["trans_level"]
assert self.state().speed is None
assert self.state().depth is None
assert self.state().dry is None
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().hardware_version == self.device.start_state["hw_version"]
assert self.state().button_pressed == self.device.start_state["button_pressed"]

assert self.state().firmware_version == device_info.firmware_version
assert self.state().firmware_version_major == device_info.firmware_version.rsplit('_', 1)[0]
assert self.state().firmware_version_minor == int(device_info.firmware_version.rsplit('_', 1)[1])
assert self.state().strong_mode_enabled is False

def test_set_mode(self):
def mode():
return self.device.status().mode

self.device.set_mode(OperationMode.Silent)
assert mode() == OperationMode.Silent

self.device.set_mode(OperationMode.Medium)
assert mode() == OperationMode.Medium

self.device.set_mode(OperationMode.High)
assert mode() == OperationMode.High

def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness

self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright

self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim

self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off

def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer

self.device.set_buzzer(True)
assert buzzer() is True

self.device.set_buzzer(False)
assert buzzer() is False

def test_status_without_temperature(self):
self.device._reset_state()
self.device.state["temp_dec"] = None

assert self.state().temperature is None

def test_status_without_led_brightness(self):
self.device._reset_state()
self.device.state["led_b"] = None

assert self.state().led_brightness is None

def test_set_target_humidity(self):
def target_humidity():
return self.device.status().target_humidity

self.device.set_target_humidity(30)
assert target_humidity() == 30
self.device.set_target_humidity(60)
assert target_humidity() == 60
self.device.set_target_humidity(80)
assert target_humidity() == 80

with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(-1)

with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(20)

with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(90)

with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(110)

def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock

self.device.set_child_lock(True)
assert child_lock() is True

self.device.set_child_lock(False)
assert child_lock() is False


class DummyAirHumidifierCA1(DummyDevice, AirHumidifier):
def __init__(self, *args, **kwargs):
self.model = MODEL_HUMIDIFIER_CA1
self.dummy_device_info = {
'fw_ver': '1.2.9_5033',
'token': '68ffffffffffffffffffffffffffffff',
'otu_stat': [101, 74, 5343, 0, 5327, 407],
'mmfree': 228248,
'netif': {'gw': '192.168.0.1',
'localIp': '192.168.0.25',
'mask': '255.255.255.0'},
'ott_stat': [0, 0, 0, 0],
'model': 'zhimi.humidifier.v1',
'cfg_time': 0,
'life': 575661,
'ap': {'rssi': -35, 'ssid': 'ap',
'bssid': 'FF:FF:FF:FF:FF:FF'},
'wifi_fw_ver': 'SD878x-14.76.36.p84-702.1.0-WM',
'hw_ver': 'MW300',
'ot': 'otu',
'mac': '78:11:FF:FF:FF:FF'
}
self.device_info = None

self.state = {
'power': 'on',
'mode': 'medium',
'temp_dec': 294,
'humidity': 33,
'buzzer': 'off',
'led_b': 2,
'child_lock': 'on',
'limit_hum': 40,
'use_time': 941100,
'hw_version': 0,
# Additional attributes of the zhimi.humidifier.ca1
'speed': 100,
'depth': 1,
Expand All @@ -69,13 +262,13 @@ def _get_device_info(self, _):


@pytest.fixture(scope="class")
def airhumidifier(request):
request.cls.device = DummyAirHumidifier()
def airhumidifierca1(request):
request.cls.device = DummyAirHumidifierCA1()
# TODO add ability to test on a real device


@pytest.mark.usefixtures("airhumidifier")
class TestAirHumidifier(TestCase):
@pytest.mark.usefixtures("airhumidifierca1")
class TestAirHumidifierCA1(TestCase):
def is_on(self):
return self.device.status().is_on

Expand Down Expand Up @@ -111,13 +304,13 @@ def test_status(self):
assert self.state().buzzer == (self.device.start_state["buzzer"] == 'on')
assert self.state().child_lock == (self.device.start_state["child_lock"] == 'on')
assert self.state().target_humidity == self.device.start_state["limit_hum"]
assert self.state().trans_level == self.device.start_state["trans_level"]
assert self.state().trans_level is None
assert self.state().speed == self.device.start_state["speed"]
assert self.state().depth == self.device.start_state["depth"]
assert self.state().dry == (self.device.start_state["dry"] == 'on')
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().hardware_version == self.device.start_state["hw_version"]
assert self.state().button_pressed == self.device.start_state["button_pressed"]
assert self.state().button_pressed is None

assert self.state().firmware_version == device_info.firmware_version
assert self.state().firmware_version_major == device_info.firmware_version.rsplit('_', 1)[0]
Expand Down Expand Up @@ -214,9 +407,3 @@ def dry():

self.device.set_dry(False)
assert dry() is False

def test_status_without_dry(self):
self.device._reset_state()
self.device.state["dry"] = None

assert self.state().dry is None

0 comments on commit 8569e75

Please sign in to comment.