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

Improve the support of the Air Humidifier CA1 (Closes: #383) #388

Merged
merged 2 commits into from
Oct 8, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (105 > 100 characters)

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'},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

'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