From 8abcc3242414e1f01335d2e62211e3e90158fb2d Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 12 Apr 2018 07:59:16 +0200 Subject: [PATCH 1/8] Refactoring --- miio/fan.py | 117 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index bc7287742..95a883892 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -5,10 +5,13 @@ import click from .click_common import command, format_output, EnumType -from .device import Device +from .device import Device, DeviceException _LOGGER = logging.getLogger(__name__) +class FanException(DeviceException): + pass + class LedBrightness(enum.Enum): Bright = 0 @@ -25,12 +28,14 @@ class FanStatus: """Container for status reports from the Xiaomi Smart Fan.""" def __init__(self, data: Dict[str, Any]) -> None: - # ['temp_dec', 'humidity', 'angle', 'speed', 'poweroff_time', 'power', - # 'ac_power', 'battery', 'angle_enable', 'speed_level', - # 'natural_level', 'child_lock', 'buzzer', 'led_b', 'led'] - # - # [232, 46, 30, 298, 0, 'on', 'off', 98, 'off', 1, 0, 'off', 'on', - # 1, 'on'] + """ + Response of a Fan (zhimi.fan.v3): + + {'temp_dec': 232, 'humidity': 46, 'angle': 30, 'speed': 298, + 'poweroff_time': 0, 'power': 'on', 'ac_power': 'off', 'battery': 98, + 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 0, + 'child_lock': 'off', 'buzzer': 'on', 'led_b': 1, 'led': 'on'} + """ self.data = data @property @@ -216,21 +221,25 @@ def off(self): @command( click.argument("speed", type=int), - default_output=format_output("Setting natural level to {level}") + default_output=format_output("Setting speed of the natural mode to {speed}") ) - def set_natural_level(self, level: int): + def set_natural_speed(self, speed: int): """Set natural level.""" - level = max(0, min(level, 100)) - return self.send("set_natural_level", [level]) # 0...100 + if speed < 0 or speed > 100: + raise FanException("Invalid speed: %s" % speed) + + return self.send("set_natural_level", [speed]) @command( click.argument("speed", type=int), - default_output=format_output("Setting speed level to {level}") + default_output=format_output("Setting speed of the direct mode to {speed}") ) - def set_speed_level(self, level: int): - """Set speed level.""" - level = max(0, min(level, 100)) - return self.send("set_speed_level", [level]) # 0...100 + def set_direct_speed(self, speed: int): + """Set speed of the direct mode.""" + if speed < 0 or speed > 100: + raise FanException("Invalid speed: %s" % speed) + + return self.send("set_speed_level", [speed]) @command( click.argument("direction", type=EnumType(MoveDirection, False)), @@ -245,23 +254,23 @@ def set_direction(self, direction: MoveDirection): click.argument("angle", type=int), default_output=format_output("Setting angle to {angle}") ) - def fan_set_angle(self, angle: int): + def set_angle(self, angle: int): """Set angle.""" return self.send("set_angle", [angle]) @command( - default_output=format_output("Turning on oscillate"), - ) - def oscillate_on(self): - """Enable oscillate.""" - return self.send("set_angle_enable", ["on"]) - - @command( - default_output=format_output("Turning off oscillate"), + click.argument("oscillate", type=bool), + default_output=format_output( + lambda lock: "Turning on oscillate" + if lock else "Turning off oscillate" + ) ) - def oscillate_off(self): - """Disable oscillate.""" - return self.send("set_angle_enable", ["off"]) + def set_oscillate(self, oscillate: bool): + """Set oscillate on/off.""" + if oscillate: + return self.send("set_angle_enable", ["on"]) + else: + return self.send("set_angle_enable", ["off"]) @command( click.argument("brightness", type=EnumType(LedBrightness, False)), @@ -273,29 +282,43 @@ def set_led_brightness(self, brightness: LedBrightness): return self.send("set_led_b", [brightness.value]) @command( - default_output=format_output("Turning on LED"), - ) - def led_on(self): - """Turn led on.""" - return self.send("set_led", ["on"]) - - @command( - default_output=format_output("Turning off LED"), + click.argument("led", type=bool), + default_output=format_output( + lambda led: "Turning on LED" + if led else "Turning off LED" + ) ) - def led_off(self): - """Turn led off.""" - return self.send("set_led", ["off"]) + def set_led(self, led: bool): + """Turn led on/off.""" + if led: + return self.send("set_led", ['on']) + else: + return self.send("set_led", ['off']) @command( - default_output=format_output("Turning on buzzer"), + click.argument("buzzer", type=bool), + default_output=format_output( + lambda buzzer: "Turning on buzzer" + if buzzer else "Turning off buzzer" + ) ) - def buzzer_on(self): - """Enable buzzer.""" - return self.send("set_buzzer", ["on"]) + def set_buzzer(self, buzzer: bool): + """Set buzzer on/off.""" + if buzzer: + return self.send("set_buzzer", ["on"]) + else: + return self.send("set_buzzer", ["off"]) @command( - default_output=format_output("Turning off buzzer"), + click.argument("lock", type=bool), + default_output=format_output( + lambda lock: "Turning on child lock" + if lock else "Turning off child lock" + ) ) - def buzzer_off(self): - """Disable buzzer.""" - return self.send("set_buzzer", ["off"]) + def set_child_lock(self, lock: bool): + """Set child lock on/off.""" + if lock: + return self.send("set_child_lock", ["on"]) + else: + return self.send("set_child_lock", ["off"]) From ebab440accd90e8e2a5fdfdb4771ef792b624757 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 12 Apr 2018 08:11:41 +0200 Subject: [PATCH 2/8] Naming of the properties improved --- miio/fan.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index 95a883892..f753963c7 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -83,13 +83,13 @@ def child_lock(self) -> bool: return self.data["child_lock"] == "on" @property - def natural_level(self) -> int: - """Fan speed in natural mode.""" + def natural_speed(self) -> int: + """Speed level in natural mode.""" return self.data["natural_level"] @property - def speed_level(self) -> int: - """Fan speed in direct mode.""" + def direct_speed(self) -> int: + """Speed level in direct mode.""" return self.data["speed_level"] @property @@ -108,14 +108,13 @@ def ac_power(self) -> bool: return self.data["ac_power"] == "on" @property - def poweroff_time(self) -> int: - """Time until turning off. FIXME verify""" + def delay_off_countdown(self) -> int: + """Countdown until turning off in seconds.""" return self.data["poweroff_time"] @property def speed(self) -> int: - """FIXME What is the meaning of this value? - (cp. speed_level vs. natural_level)""" + """Speed of the motor.""" return self.data["speed"] @property @@ -131,12 +130,12 @@ def __str__(self) -> str: "led_brightness=%s, " \ "buzzer=%s, " \ "child_lock=%s, " \ - "natural_level=%s, " \ - "speed_level=%s, " \ + "natural_speed=%s, " \ + "direct_speed=%s, " \ "oscillate=%s, " \ "battery=%s, " \ "ac_power=%s, " \ - "poweroff_time=%s, " \ + "delay_off_countdown=%s, " \ "speed=%s, " \ "angle=%s" % \ (self.power, @@ -146,13 +145,13 @@ def __str__(self) -> str: self.led_brightness, self.buzzer, self.child_lock, - self.natural_level, - self.speed_level, + self.natural_speed, + self.direct_speed, self.oscillate, self.battery, self.ac_power, - self.poweroff_time, - self.speed_level, + self.delay_off_countdown, + self.speed, self.angle) return s @@ -322,3 +321,16 @@ def set_child_lock(self, lock: bool): return self.send("set_child_lock", ["on"]) else: return self.send("set_child_lock", ["off"]) + + @command( + click.argument("seconds", type=int), + default_output=format_output("Setting delayed turn off to {seconds} seconds") + ) + def delay_off(self, seconds: int): + """Set delay off seconds.""" + + if seconds < 1: + raise FanException( + "Invalid value for a delayed turn off: %s" % seconds) + + return self.send("poweroff_time", [seconds]) From 8938ec4d8ddc65387cee5c820760ce69072d745d Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 14 Apr 2018 13:10:48 +0200 Subject: [PATCH 3/8] Available property updated --- miio/fan.py | 131 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 111 insertions(+), 20 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index f753963c7..214cd4fac 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -9,6 +9,53 @@ _LOGGER = logging.getLogger(__name__) +MODEL_FAN_V2 = 'zimi.fan.v2' +MODEL_FAN_V3 = 'zimi.fan.v3' + +AVAILABLE_PROPERTIES = { + MODEL_FAN_V2: [ + 'temp_dec', + 'humidity', + 'angle', + 'speed', + 'poweroff_time', + 'power', + 'ac_power', + 'battery', + 'angle_enable', + 'speed_level', + 'natural_level', + 'child_lock', + 'buzzer', + 'led_b', + 'led', + 'use_time', + 'bat_charge', + 'bat_state', + 'button_pressed', + ], + MODEL_FAN_V3: [ + 'temp_dec', + 'humidity', + 'angle', + 'speed', + 'poweroff_time', + 'power', + 'ac_power', + 'battery', + 'angle_enable', + 'speed_level', + 'natural_level', + 'child_lock', + 'buzzer', + 'led_b', + 'use_time', + 'bat_charge', + 'button_pressed', + ], +} + + class FanException(DeviceException): pass @@ -31,10 +78,12 @@ def __init__(self, data: Dict[str, Any]) -> None: """ Response of a Fan (zhimi.fan.v3): - {'temp_dec': 232, 'humidity': 46, 'angle': 30, 'speed': 298, + {'temp_dec': 232, 'humidity': 46, 'angle': 118, 'speed': 298, 'poweroff_time': 0, 'power': 'on', 'ac_power': 'off', 'battery': 98, 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 0, - 'child_lock': 'off', 'buzzer': 'on', 'led_b': 1, 'led': 'on'} + 'child_lock': 'off', 'buzzer': 'on', 'led_b': 1, 'led': None, + 'natural_enable': None, 'use_time': 0, : 118, + 'bat_charge': 'complete', 'bat_state': None, 'button_pressed':'speed'} """ self.data = data @@ -61,9 +110,11 @@ def temperature(self) -> Optional[float]: return None @property - def led(self) -> bool: - """True if LED is turned on.""" - return self.data["led"] == "on" + def led(self) -> Optional[bool]: + """True if LED is turned on, if available.""" + if self.data["led"] is not None: + return self.data["led"] == "on" + return None @property def led_brightness(self) -> Optional[LedBrightness]: @@ -102,6 +153,20 @@ def battery(self) -> int: """Current battery level.""" return self.data["battery"] + @property + def battery_charge(self) -> Optional[str]: + """State of the battery charger, if available.""" + if self.data["bat_charge"] is not None: + return self.data["bat_charge"] + return None + + @property + def battery_state(self) -> Optional[str]: + """State of the battery, if available.""" + if self.data["bat_state"] is not None: + return self.data["bat_state"] + return None + @property def ac_power(self) -> bool: """True if powered by AC.""" @@ -122,6 +187,18 @@ def angle(self) -> int: """Current angle.""" return self.data["angle"] + @property + def use_time(self) -> int: + """How long the device has been active in seconds.""" + return self.data["use_time"] + + @property + def button_pressed(self) -> Optional[str]: + """Last pressed button.""" + if self.data["button_pressed"] is not None: + return self.data["button_pressed"] + return None + def __str__(self) -> str: s = " str: "child_lock=%s, " \ "natural_speed=%s, " \ "direct_speed=%s, " \ + "speed=%s, " \ "oscillate=%s, " \ - "battery=%s, " \ + "angle=%s, " \ "ac_power=%s, " \ + "battery=%s, " \ + "battery_charge=%s, " \ + "battery_state=%s, " \ + "use_time=%s, " \ "delay_off_countdown=%s, " \ - "speed=%s, " \ - "angle=%s" % \ + "button_pressed=%s>" % \ (self.power, self.temperature, self.humidity, @@ -147,12 +228,16 @@ def __str__(self) -> str: self.child_lock, self.natural_speed, self.direct_speed, + self.speed, self.oscillate, - self.battery, + self.angle, self.ac_power, + self.battery, + self.battery_charge, + self.battery_state, + self.use_time, self.delay_off_countdown, - self.speed, - self.angle) + self.button_pressed) return s def __json__(self): @@ -162,6 +247,16 @@ def __json__(self): class Fan(Device): """Main class representing the Xiaomi Smart Fan.""" + def __init__(self, ip: str = None, token: str = None, start_id: int = 0, + debug: int = 0, lazy_discover: bool = True, + model: str = MODEL_FAN_V3) -> None: + super().__init__(ip, token, start_id, debug, lazy_discover) + + if model in AVAILABLE_PROPERTIES: + self.model = model + else: + self.model = MODEL_FAN_V3 + @command( default_output=format_output( "", @@ -184,11 +279,7 @@ class Fan(Device): ) def status(self) -> FanStatus: """Retrieve properties.""" - properties = ['temp_dec', 'humidity', 'angle', 'speed', - 'poweroff_time', 'power', 'ac_power', 'battery', - 'angle_enable', 'speed_level', 'natural_level', - 'child_lock', 'buzzer', 'led_b', 'led'] - + properties = AVAILABLE_PROPERTIES[self.model] values = self.send( "get_prop", properties @@ -243,10 +334,10 @@ def set_direct_speed(self, speed: int): @command( click.argument("direction", type=EnumType(MoveDirection, False)), default_output=format_output( - "Setting move direction to {direction}") + "Rotating the fan by 5 degrees to the {direction}") ) - def set_direction(self, direction: MoveDirection): - """Set move direction.""" + def set_rotate(self, direction: MoveDirection): + """Rotate the fan by 5 degrees left/right.""" return self.send("set_move", [direction.value]) @command( @@ -254,7 +345,7 @@ def set_direction(self, direction: MoveDirection): default_output=format_output("Setting angle to {angle}") ) def set_angle(self, angle: int): - """Set angle.""" + """Set the oscillation angle.""" return self.send("set_angle", [angle]) @command( From df18b991c6d63d7d82521dfa890e8bbe0092ba9f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 14 Apr 2018 13:37:10 +0200 Subject: [PATCH 4/8] Tests added --- miio/fan.py | 10 +-- miio/tests/test_fan.py | 180 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 miio/tests/test_fan.py diff --git a/miio/fan.py b/miio/fan.py index 214cd4fac..b24cbf500 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -82,8 +82,8 @@ def __init__(self, data: Dict[str, Any]) -> None: 'poweroff_time': 0, 'power': 'on', 'ac_power': 'off', 'battery': 98, 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 0, 'child_lock': 'off', 'buzzer': 'on', 'led_b': 1, 'led': None, - 'natural_enable': None, 'use_time': 0, : 118, - 'bat_charge': 'complete', 'bat_state': None, 'button_pressed':'speed'} + 'natural_enable': None, 'use_time': 0, 'bat_charge': 'complete', + 'bat_state': None, 'button_pressed':'speed'} """ self.data = data @@ -112,7 +112,7 @@ def temperature(self) -> Optional[float]: @property def led(self) -> Optional[bool]: """True if LED is turned on, if available.""" - if self.data["led"] is not None: + if "led" in self.data and self.data["led"] is not None: return self.data["led"] == "on" return None @@ -163,7 +163,7 @@ def battery_charge(self) -> Optional[str]: @property def battery_state(self) -> Optional[str]: """State of the battery, if available.""" - if self.data["bat_state"] is not None: + if "bat_state" in self.data and self.data["bat_state"] is not None: return self.data["bat_state"] return None @@ -199,7 +199,7 @@ def button_pressed(self) -> Optional[str]: return self.data["button_pressed"] return None - def __str__(self) -> str: + def __repr__(self) -> str: s = " Date: Sat, 14 Apr 2018 14:10:59 +0200 Subject: [PATCH 5/8] Test coverage improved --- miio/fan.py | 13 +- miio/tests/test_fan.py | 301 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 305 insertions(+), 9 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index b24cbf500..c85949218 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -103,16 +103,14 @@ def humidity(self) -> int: return self.data["humidity"] @property - def temperature(self) -> Optional[float]: + def temperature(self) -> float: """Current temperature, if available.""" - if self.data["temp_dec"] is not None: - return self.data["temp_dec"] / 10.0 - return None + return self.data["temp_dec"] / 10.0 @property def led(self) -> Optional[bool]: """True if LED is turned on, if available.""" - if "led" in self.data and self.data["led"] is not None: + if "led" in self.data and self.data["led"] is not None: return self.data["led"] == "on" return None @@ -346,6 +344,9 @@ def set_rotate(self, direction: MoveDirection): ) def set_angle(self, angle: int): """Set the oscillation angle.""" + if angle < 0 or angle > 120: + raise FanException("Invalid angle: %s" % angle) + return self.send("set_angle", [angle]) @command( @@ -424,4 +425,4 @@ def delay_off(self, seconds: int): raise FanException( "Invalid value for a delayed turn off: %s" % seconds) - return self.send("poweroff_time", [seconds]) + return self.send("set_poweroff_time", [seconds]) diff --git a/miio/tests/test_fan.py b/miio/tests/test_fan.py index 1006de89a..c827b41f3 100644 --- a/miio/tests/test_fan.py +++ b/miio/tests/test_fan.py @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): 'child_lock': 'off', 'buzzer': 'on', 'led_b': 1, - 'led': None, + 'led': 'on', 'natural_enable': None, 'use_time': 0, 'bat_charge': 'complete', @@ -37,6 +37,16 @@ def __init__(self, *args, **kwargs): self.return_values = { 'get_prop': self._get_state, 'set_power': lambda x: self._set_state("power", x), + 'set_speed_level': lambda x: self._set_state("speed_level", x), + 'set_natural_level': lambda x: self._set_state("natural_level", x), + 'set_move': lambda x: True, + 'set_angle': lambda x: self._set_state("angle", x), + 'set_angle_enable': lambda x: self._set_state("angle_enable", x), + 'set_led_b': lambda x: self._set_state("led_b", x), + 'set_led': lambda x: self._set_state("led", x), + 'set_buzzer': lambda x: self._set_state("buzzer", x), + 'set_child_lock': lambda x: self._set_state("child_lock", x), + 'set_poweroff_time': lambda x: self._set_state("poweroff_time", x), } super().__init__(args, kwargs) @@ -88,12 +98,155 @@ def test_status(self): assert self.state().child_lock is (self.device.start_state["child_lock"] == 'on') assert self.state().buzzer is (self.device.start_state["buzzer"] == 'on') assert self.state().led_brightness == LedBrightness(self.device.start_state["led_b"]) - assert self.state().led == self.device.start_state["led"] + assert self.state().led is (self.device.start_state["led"] == "on") assert self.state().use_time == self.device.start_state["use_time"] assert self.state().battery_charge == self.device.start_state["bat_charge"] assert self.state().battery_state == self.device.start_state["bat_state"] assert self.state().button_pressed == self.device.start_state["button_pressed"] + 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_status_without_battery_charge(self): + self.device._reset_state() + + self.device.state["bat_charge"] = None + assert self.state().battery_charge is None + + def test_status_without_battery_state(self): + self.device._reset_state() + + self.device.state["bat_state"] = None + assert self.state().battery_state is None + + def test_status_without_button_pressed(self): + self.device._reset_state() + + self.device.state["button_pressed"] = None + assert self.state().button_pressed is None + + def test_set_led(self): + def led(): + return self.device.status().led + + self.device.set_led(True) + assert led() is True + + self.device.set_led(False) + assert led() is False + + def test_set_direct_speed(self): + def direct_speed(): + return self.device.status().direct_speed + + self.device.set_direct_speed(0) + assert direct_speed() == 0 + self.device.set_direct_speed(1) + assert direct_speed() == 1 + self.device.set_direct_speed(100) + assert direct_speed() == 100 + + with pytest.raises(FanException): + self.device.set_direct_speed(-1) + + with pytest.raises(FanException): + self.device.set_direct_speed(101) + + def test_set_rotate(self): + """The method is open-loop. The new state cannot be retrieved.""" + self.device.set_rotate(MoveDirection.Left) + self.device.set_rotate(MoveDirection.Right) + + def test_set_direct_speed(self): + """This test doesn't implement the real behaviour of the device may be. + + The property "angle" doesn't provide the current setting. + It's a measurement of the current position probably. + """ + def angle(): + return self.device.status().angle + + self.device.set_angle(0) # TODO: Is this value allowed? + assert angle() == 0 + self.device.set_angle(1) # TODO: Is this value allowed? + assert angle() == 1 + self.device.set_angle(30) + assert angle() == 30 + self.device.set_angle(60) + assert angle() == 60 + self.device.set_angle(90) + assert angle() == 90 + self.device.set_angle(120) + assert angle() == 120 + + with pytest.raises(FanException): + self.device.set_angle(-1) + + with pytest.raises(FanException): + self.device.set_angle(121) + + def test_set_oscillate(self): + def oscillate(): + return self.device.status().oscillate + + self.device.set_oscillate(True) + assert oscillate() is True + + self.device.set_oscillate(False) + assert oscillate() is False + + 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_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 + + def test_delay_off(self): + def delay_off_countdown(): + return self.device.status().delay_off_countdown + + self.device.delay_off(100) + assert delay_off_countdown() == 100 + self.device.delay_off(200) + assert delay_off_countdown() == 200 + + with pytest.raises(FanException): + self.device.delay_off(-1) + + with pytest.raises(FanException): + self.device.delay_off(0) + + class DummyFanV3(DummyDevice, Fan): def __init__(self, *args, **kwargs): self.model = MODEL_FAN_V3 @@ -122,6 +275,16 @@ def __init__(self, *args, **kwargs): self.return_values = { 'get_prop': self._get_state, 'set_power': lambda x: self._set_state("power", x), + 'set_speed_level': lambda x: self._set_state("speed_level", x), + 'set_natural_level': lambda x: self._set_state("natural_level", x), + 'set_move': lambda x: True, + 'set_angle': lambda x: self._set_state("angle", x), + 'set_angle_enable': lambda x: self._set_state("angle_enable", x), + 'set_led_b': lambda x: self._set_state("led_b", x), + 'set_led': lambda x: self._set_state("led", x), + 'set_buzzer': lambda x: self._set_state("buzzer", x), + 'set_child_lock': lambda x: self._set_state("child_lock", x), + 'set_poweroff_time': lambda x: self._set_state("poweroff_time", x), } super().__init__(args, kwargs) @@ -173,8 +336,140 @@ def test_status(self): assert self.state().child_lock is (self.device.start_state["child_lock"] == 'on') assert self.state().buzzer is (self.device.start_state["buzzer"] == 'on') assert self.state().led_brightness == LedBrightness(self.device.start_state["led_b"]) - assert self.state().led == self.device.start_state["led"] + assert self.state().led is None assert self.state().use_time == self.device.start_state["use_time"] assert self.state().battery_charge == self.device.start_state["bat_charge"] assert self.state().battery_state == self.device.start_state["bat_state"] assert self.state().button_pressed == self.device.start_state["button_pressed"] + + 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_status_without_battery_charge(self): + self.device._reset_state() + + self.device.state["bat_charge"] = None + assert self.state().battery_charge is None + + def test_status_without_battery_state(self): + self.device._reset_state() + + self.device.state["bat_state"] = None + assert self.state().battery_state is None + + def test_status_without_button_pressed(self): + self.device._reset_state() + + self.device.state["button_pressed"] = None + assert self.state().button_pressed is None + + def test_set_direct_speed(self): + def direct_speed(): + return self.device.status().direct_speed + + self.device.set_direct_speed(0) + assert direct_speed() == 0 + self.device.set_direct_speed(1) + assert direct_speed() == 1 + self.device.set_direct_speed(100) + assert direct_speed() == 100 + + with pytest.raises(FanException): + self.device.set_direct_speed(-1) + + with pytest.raises(FanException): + self.device.set_direct_speed(101) + + def test_set_rotate(self): + """The method is open-loop. The new state cannot be retrieved.""" + self.device.set_rotate(MoveDirection.Left) + self.device.set_rotate(MoveDirection.Right) + + def test_set_direct_speed(self): + """This test doesn't implement the real behaviour of the device may be. + + The property "angle" doesn't provide the current setting. + It's a measurement of the current position probably. + """ + def angle(): + return self.device.status().angle + + self.device.set_angle(0) # TODO: Is this value allowed? + assert angle() == 0 + self.device.set_angle(1) # TODO: Is this value allowed? + assert angle() == 1 + self.device.set_angle(30) + assert angle() == 30 + self.device.set_angle(60) + assert angle() == 60 + self.device.set_angle(90) + assert angle() == 90 + self.device.set_angle(120) + assert angle() == 120 + + with pytest.raises(FanException): + self.device.set_angle(-1) + + with pytest.raises(FanException): + self.device.set_angle(121) + + def test_set_oscillate(self): + def oscillate(): + return self.device.status().oscillate + + self.device.set_oscillate(True) + assert oscillate() is True + + self.device.set_oscillate(False) + assert oscillate() is False + + 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_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 + + def test_delay_off(self): + def delay_off_countdown(): + return self.device.status().delay_off_countdown + + self.device.delay_off(100) + assert delay_off_countdown() == 100 + self.device.delay_off(200) + assert delay_off_countdown() == 200 + + with pytest.raises(FanException): + self.device.delay_off(-1) + + with pytest.raises(FanException): + self.device.delay_off(0) From 2f4c9b3254bc55e923b6a0f3e01d1c75bf68dd44 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 14 Apr 2018 14:17:42 +0200 Subject: [PATCH 6/8] Method name fixed --- miio/tests/test_fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/tests/test_fan.py b/miio/tests/test_fan.py index c827b41f3..ccb1ab5c3 100644 --- a/miio/tests/test_fan.py +++ b/miio/tests/test_fan.py @@ -160,7 +160,7 @@ def test_set_rotate(self): self.device.set_rotate(MoveDirection.Left) self.device.set_rotate(MoveDirection.Right) - def test_set_direct_speed(self): + def test_set_angle(self): """This test doesn't implement the real behaviour of the device may be. The property "angle" doesn't provide the current setting. @@ -388,7 +388,7 @@ def test_set_rotate(self): self.device.set_rotate(MoveDirection.Left) self.device.set_rotate(MoveDirection.Right) - def test_set_direct_speed(self): + def test_set_angle(self): """This test doesn't implement the real behaviour of the device may be. The property "angle" doesn't provide the current setting. From 14f41445799d9802e3c4c467c12e6f53366e9993 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 16 Apr 2018 08:39:58 +0200 Subject: [PATCH 7/8] Review incorporated --- miio/fan.py | 75 +++++++++++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index c85949218..d9f9cbd9b 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -12,47 +12,29 @@ MODEL_FAN_V2 = 'zimi.fan.v2' MODEL_FAN_V3 = 'zimi.fan.v3' +AVAILABLE_PROPERTIES_COMMON = [ + 'temp_dec', + 'humidity', + 'angle', + 'speed', + 'poweroff_time', + 'power', + 'ac_power', + 'battery', + 'angle_enable', + 'speed_level', + 'natural_level', + 'child_lock', + 'buzzer', + 'led_b', + 'use_time', + 'bat_charge', + 'button_pressed', +] + AVAILABLE_PROPERTIES = { - MODEL_FAN_V2: [ - 'temp_dec', - 'humidity', - 'angle', - 'speed', - 'poweroff_time', - 'power', - 'ac_power', - 'battery', - 'angle_enable', - 'speed_level', - 'natural_level', - 'child_lock', - 'buzzer', - 'led_b', - 'led', - 'use_time', - 'bat_charge', - 'bat_state', - 'button_pressed', - ], - MODEL_FAN_V3: [ - 'temp_dec', - 'humidity', - 'angle', - 'speed', - 'poweroff_time', - 'power', - 'ac_power', - 'battery', - 'angle_enable', - 'speed_level', - 'natural_level', - 'child_lock', - 'buzzer', - 'led_b', - 'use_time', - 'bat_charge', - 'button_pressed', - ], + MODEL_FAN_V2: ['led', 'bat_state'] + AVAILABLE_PROPERTIES_COMMON, + MODEL_FAN_V3: AVAILABLE_PROPERTIES_COMMON, } @@ -309,7 +291,8 @@ def off(self): @command( click.argument("speed", type=int), - default_output=format_output("Setting speed of the natural mode to {speed}") + default_output=format_output( + "Setting speed of the natural mode to {speed}") ) def set_natural_speed(self, speed: int): """Set natural level.""" @@ -320,7 +303,8 @@ def set_natural_speed(self, speed: int): @command( click.argument("speed", type=int), - default_output=format_output("Setting speed of the direct mode to {speed}") + default_output=format_output( + "Setting speed of the direct mode to {speed}") ) def set_direct_speed(self, speed: int): """Set speed of the direct mode.""" @@ -332,10 +316,10 @@ def set_direct_speed(self, speed: int): @command( click.argument("direction", type=EnumType(MoveDirection, False)), default_output=format_output( - "Rotating the fan by 5 degrees to the {direction}") + "Rotating the fan to the {direction}") ) def set_rotate(self, direction: MoveDirection): - """Rotate the fan by 5 degrees left/right.""" + """Rotate the fan by -5/+5 degrees left/right.""" return self.send("set_move", [direction.value]) @command( @@ -416,7 +400,8 @@ def set_child_lock(self, lock: bool): @command( click.argument("seconds", type=int), - default_output=format_output("Setting delayed turn off to {seconds} seconds") + default_output=format_output( + "Setting delayed turn off to {seconds} seconds") ) def delay_off(self, seconds: int): """Set delay off seconds.""" From f1a2398f3c5ccf15881fb33065ddae80e2b7c670 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 16 Apr 2018 08:46:41 +0200 Subject: [PATCH 8/8] Some minor changes --- README.rst | 5 +++-- miio/discovery.py | 5 +++-- miio/fan.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 9f3bc0ccd..75c153b4f 100644 --- a/README.rst +++ b/README.rst @@ -27,7 +27,7 @@ Supported devices - Xiaomi Philips LED Ball Lamp (:class:`miio.philips_bulb`) - Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp (:class:`miio.philips_bulb`) - Xiaomi Universal IR Remote Controller (Chuangmi IR) (:class:`miio.chuangmi_ir`) -- Xiaomi Mi Smart Fan (:class:`miio.fan`) +- Xiaomi Mi Smart Pedestal Fan (:class:`miio.fan`) - Xiaomi Mi Air Humidifier (:class:`miio.airhumidifier`) - Xiaomi Mi Water Purifier (Basic support: Turn on & off) (:class:`miio.waterpurifier`) - Xiaomi PM2.5 Air Quality Monitor (:class:`miio.airqualitymonitor`) @@ -55,7 +55,8 @@ Home Assistant support - `Xiaomi Universal IR Remote Controller `__ - `Xiaomi Mi Air Quality Monitor (PM2.5) `__ - `Xiaomi Mi Home Air Conditioner Companion `__ -- `Xiaomi Mi WiFi Repeater 2 `__ +- `Xiaomi Mi WiFi Repeater 2 `__ +- `Xiaomi Mi Smart Pedestal Fan `__ .. |PyPI version| image:: https://badge.fury.io/py/python-miio.svg diff --git a/miio/discovery.py b/miio/discovery.py index 40b4aa0ce..b5cf7d793 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -12,6 +12,7 @@ WaterPurifier, WifiSpeaker, WifiRepeater, Yeelight, Fan, ) from .chuangmi_plug import (MODEL_CHUANGMI_PLUG_V1, MODEL_CHUANGMI_PLUG_V3, MODEL_CHUANGMI_PLUG_M1, ) +from .fan import (MODEL_FAN_V2, MODEL_FAN_V3, ) from .powerstrip import (MODEL_POWER_STRIP_V1, MODEL_POWER_STRIP_V2, ) _LOGGER = logging.getLogger(__name__) @@ -52,8 +53,8 @@ "xiaomi-repeater-v1": WifiRepeater, # name needs to be checked "xiaomi-repeater-v3": WifiRepeater, # name needs to be checked "yeelink-light-": Yeelight, - "zhimi-fan-v2": Fan, - "zhimi-fan-v3": Fan, + "zhimi-fan-v2": partial(Fan, model=MODEL_FAN_V2), + "zhimi-fan-v3": partial(Fan, model=MODEL_FAN_V3), "lumi-gateway-": lambda x: other_package_info( x, "https://github.com/Danielhiversen/PyXiaomiGateway") } # type: Dict[str, Union[Callable, Device]] diff --git a/miio/fan.py b/miio/fan.py index d9f9cbd9b..be8720a1d 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -54,7 +54,7 @@ class MoveDirection(enum.Enum): class FanStatus: - """Container for status reports from the Xiaomi Smart Fan.""" + """Container for status reports from the Xiaomi Mi Smart Pedestal Fan.""" def __init__(self, data: Dict[str, Any]) -> None: """ @@ -225,7 +225,7 @@ def __json__(self): class Fan(Device): - """Main class representing the Xiaomi Smart Fan.""" + """Main class representing the Xiaomi Mi Smart Pedestal Fan.""" def __init__(self, ip: str = None, token: str = None, start_id: int = 0, debug: int = 0, lazy_discover: bool = True,