From dc4872d179c705521fe5c83d283b120390c03e1f Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sun, 16 Jan 2022 18:45:45 +0100 Subject: [PATCH] Split fan.py to vendor-specific fan integrations --- miio/__init__.py | 10 +- miio/discovery.py | 26 +- miio/integrations/fan/dmaker/__init__.py | 1 + miio/integrations/fan/dmaker/fan.py | 242 +++++++++++++++++ miio/integrations/fan/dmaker/test_fan.py | 190 +++++++++++++ miio/integrations/fan/zhimi/__init__.py | 3 +- miio/{ => integrations/fan/zhimi}/fan.py | 253 +----------------- .../fan/zhimi}/test_fan.py | 193 +------------ 8 files changed, 458 insertions(+), 460 deletions(-) create mode 100644 miio/integrations/fan/dmaker/fan.py create mode 100644 miio/integrations/fan/dmaker/test_fan.py rename miio/{ => integrations/fan/zhimi}/fan.py (64%) rename miio/{tests => integrations/fan/zhimi}/test_fan.py (81%) diff --git a/miio/__init__.py b/miio/__init__.py index 15ce0c6b3..d89468747 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -31,16 +31,14 @@ from miio.chuangmi_plug import ChuangmiPlug, Plug, PlugV1, PlugV3 from miio.cooker import Cooker from miio.curtain_youpin import CurtainMiot -from miio.device import Device, DeviceStatus from miio.exceptions import DeviceError, DeviceException -from miio.fan import Fan, FanP5, FanSA1, FanV2, FanZA1, FanZA4 from miio.fan_leshow import FanLeshow from miio.gateway import Gateway from miio.heater import Heater from miio.heater_miot import HeaterMiot from miio.huizuo import Huizuo, HuizuoLampFan, HuizuoLampHeater, HuizuoLampScene -from miio.integrations.fan.dmaker import Fan1C, FanMiot, FanP9, FanP10, FanP11 -from miio.integrations.fan.zhimi import FanZA5 +from miio.integrations.fan.dmaker import Fan1C, FanMiot, FanP5, FanP9, FanP10, FanP11 +from miio.integrations.fan.zhimi import Fan, FanSA1, FanV2, FanZA1, FanZA4, FanZA5 from miio.integrations.petwaterdispenser import PetWaterDispenser from miio.integrations.vacuum.dreame.dreamevacuum_miot import DreameVacuumMiot from miio.integrations.vacuum.mijia import G1Vacuum @@ -56,7 +54,6 @@ from miio.integrations.vacuum.roidmi.roidmivacuum_miot import RoidmiVacuumMiot from miio.integrations.vacuum.viomi.viomivacuum import ViomiVacuum from miio.integrations.yeelight import Yeelight -from miio.miot_device import MiotDevice from miio.philips_bulb import PhilipsBulb, PhilipsWhiteBulb from miio.philips_eyecare import PhilipsEyecare from miio.philips_moonlight import PhilipsMoonlight @@ -73,6 +70,9 @@ from miio.wifispeaker import WifiSpeaker from miio.yeelight_dual_switch import YeelightDualControlModule +from .device import Device, DeviceStatus +from .miot_device import MiotDevice + from miio.discovery import Discovery __version__ = version("python-miio") diff --git a/miio/discovery.py b/miio/discovery.py index e07c77c76..313c489a9 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -32,7 +32,6 @@ ChuangmiPlug, Cooker, Device, - Fan, FanLeshow, Gateway, Heater, @@ -78,18 +77,9 @@ MODEL_CHUANGMI_PLUG_V2, MODEL_CHUANGMI_PLUG_V3, ) -from .fan import ( - MODEL_FAN_P5, - MODEL_FAN_SA1, - MODEL_FAN_V2, - MODEL_FAN_V3, - MODEL_FAN_ZA1, - MODEL_FAN_ZA3, - MODEL_FAN_ZA4, -) from .heater import MODEL_HEATER_MA1, MODEL_HEATER_ZA1 from .integrations.fan.dmaker import FanMiot -from .integrations.fan.zhimi import FanZA5 +from .integrations.fan.zhimi import Fan, FanZA5 from .powerstrip import MODEL_POWER_STRIP_V1, MODEL_POWER_STRIP_V2 from .toiletlid import MODEL_TOILETLID_V1 @@ -178,14 +168,14 @@ "lumi-camera-aq2": AqaraCamera, "yeelink-light-": Yeelight, "leshow-fan-ss4": FanLeshow, - "zhimi-fan-v2": partial(Fan, model=MODEL_FAN_V2), - "zhimi-fan-v3": partial(Fan, model=MODEL_FAN_V3), - "zhimi-fan-sa1": partial(Fan, model=MODEL_FAN_SA1), - "zhimi-fan-za1": partial(Fan, model=MODEL_FAN_ZA1), - "zhimi-fan-za3": partial(Fan, model=MODEL_FAN_ZA3), - "zhimi-fan-za4": partial(Fan, model=MODEL_FAN_ZA4), + "zhimi-fan-v2": Fan, + "zhimi-fan-v3": Fan, + "zhimi-fan-sa1": Fan, + "zhimi-fan-za1": Fan, + "zhimi-fan-za3": Fan, + "zhimi-fan-za4": Fan, "dmaker-fan-1c": FanMiot, - "dmaker-fan-p5": partial(Fan, model=MODEL_FAN_P5), + "dmaker-fan-p5": Fan, "dmaker-fan-p9": FanMiot, "dmaker-fan-p10": FanMiot, "dmaker-fan-p11": FanMiot, diff --git a/miio/integrations/fan/dmaker/__init__.py b/miio/integrations/fan/dmaker/__init__.py index 0b938013b..f4abffd15 100644 --- a/miio/integrations/fan/dmaker/__init__.py +++ b/miio/integrations/fan/dmaker/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa +from .fan import FanP5 from .fan_miot import Fan1C, FanMiot, FanP9, FanP10, FanP11 diff --git a/miio/integrations/fan/dmaker/fan.py b/miio/integrations/fan/dmaker/fan.py new file mode 100644 index 000000000..efe12bcf2 --- /dev/null +++ b/miio/integrations/fan/dmaker/fan.py @@ -0,0 +1,242 @@ +from typing import Any, Dict + +import click + +from miio import Device, DeviceStatus +from miio.click_common import EnumType, command, format_output +from miio.fan_common import FanException, MoveDirection, OperationMode + +MODEL_FAN_P5 = "dmaker.fan.p5" + +AVAILABLE_PROPERTIES_P5 = [ + "power", + "mode", + "speed", + "roll_enable", + "roll_angle", + "time_off", + "light", + "beep_sound", + "child_lock", +] + +AVAILABLE_PROPERTIES = { + MODEL_FAN_P5: AVAILABLE_PROPERTIES_P5, +} + + +class FanStatusP5(DeviceStatus): + """Container for status reports from the Xiaomi Mi Smart Pedestal Fan DMaker P5.""" + + def __init__(self, data: Dict[str, Any]) -> None: + """Response of a Fan (dmaker.fan.p5): + + {'power': False, 'mode': 'normal', 'speed': 35, 'roll_enable': False, + 'roll_angle': 140, 'time_off': 0, 'light': True, 'beep_sound': False, + 'child_lock': False} + """ + self.data = data + + @property + def power(self) -> str: + """Power state.""" + return "on" if self.data["power"] else "off" + + @property + def is_on(self) -> bool: + """True if device is currently on.""" + return self.data["power"] + + @property + def mode(self) -> OperationMode: + """Operation mode.""" + return OperationMode(self.data["mode"]) + + @property + def speed(self) -> int: + """Speed of the motor.""" + return self.data["speed"] + + @property + def oscillate(self) -> bool: + """True if oscillation is enabled.""" + return self.data["roll_enable"] + + @property + def angle(self) -> int: + """Oscillation angle.""" + return self.data["roll_angle"] + + @property + def delay_off_countdown(self) -> int: + """Countdown until turning off in seconds.""" + return self.data["time_off"] + + @property + def led(self) -> bool: + """True if LED is turned on, if available.""" + return self.data["light"] + + @property + def buzzer(self) -> bool: + """True if buzzer is turned on.""" + return self.data["beep_sound"] + + @property + def child_lock(self) -> bool: + """True if child lock is on.""" + return self.data["child_lock"] + + +class FanP5(Device): + """Support for dmaker.fan.p5.""" + + _supported_models = [MODEL_FAN_P5] + + def __init__( + self, + ip: str = None, + token: str = None, + start_id: int = 0, + debug: int = 0, + lazy_discover: bool = True, + model: str = MODEL_FAN_P5, + ) -> None: + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) + + @command( + default_output=format_output( + "", + "Power: {result.power}\n" + "Operation mode: {result.mode}\n" + "Speed: {result.speed}\n" + "Oscillate: {result.oscillate}\n" + "Angle: {result.angle}\n" + "LED: {result.led}\n" + "Buzzer: {result.buzzer}\n" + "Child lock: {result.child_lock}\n" + "Power-off time: {result.delay_off_countdown}\n", + ) + ) + def status(self) -> FanStatusP5: + """Retrieve properties.""" + properties = AVAILABLE_PROPERTIES[self.model] + values = self.get_properties(properties, max_properties=15) + + return FanStatusP5(dict(zip(properties, values))) + + @command(default_output=format_output("Powering on")) + def on(self): + """Power on.""" + return self.send("s_power", [True]) + + @command(default_output=format_output("Powering off")) + def off(self): + """Power off.""" + return self.send("s_power", [False]) + + @command( + click.argument("mode", type=EnumType(OperationMode)), + default_output=format_output("Setting mode to '{mode.value}'"), + ) + def set_mode(self, mode: OperationMode): + """Set mode.""" + return self.send("s_mode", [mode.value]) + + @command( + click.argument("speed", type=int), + default_output=format_output("Setting speed to {speed}"), + ) + def set_speed(self, speed: int): + """Set speed.""" + if speed < 0 or speed > 100: + raise FanException("Invalid speed: %s" % speed) + + return self.send("s_speed", [speed]) + + @command( + click.argument("angle", type=int), + default_output=format_output("Setting angle to {angle}"), + ) + def set_angle(self, angle: int): + """Set the oscillation angle.""" + if angle not in [30, 60, 90, 120, 140]: + raise FanException( + "Unsupported angle. Supported values: 30, 60, 90, 120, 140" + ) + + return self.send("s_angle", [angle]) + + @command( + click.argument("oscillate", type=bool), + default_output=format_output( + lambda oscillate: "Turning on oscillate" + if oscillate + else "Turning off oscillate" + ), + ) + def set_oscillate(self, oscillate: bool): + """Set oscillate on/off.""" + if oscillate: + return self.send("s_roll", [True]) + else: + return self.send("s_roll", [False]) + + @command( + click.argument("led", type=bool), + default_output=format_output( + lambda led: "Turning on LED" if led else "Turning off LED" + ), + ) + def set_led(self, led: bool): + """Turn led on/off.""" + if led: + return self.send("s_light", [True]) + else: + return self.send("s_light", [False]) + + @command( + click.argument("buzzer", type=bool), + default_output=format_output( + lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer" + ), + ) + def set_buzzer(self, buzzer: bool): + """Set buzzer on/off.""" + if buzzer: + return self.send("s_sound", [True]) + else: + return self.send("s_sound", [False]) + + @command( + click.argument("lock", type=bool), + default_output=format_output( + lambda lock: "Turning on child lock" if lock else "Turning off child lock" + ), + ) + def set_child_lock(self, lock: bool): + """Set child lock on/off.""" + if lock: + return self.send("s_lock", [True]) + else: + return self.send("s_lock", [False]) + + @command( + click.argument("minutes", type=int), + default_output=format_output("Setting delayed turn off to {minutes} minutes"), + ) + def delay_off(self, minutes: int): + """Set delay off minutes.""" + + if minutes < 0: + raise FanException("Invalid value for a delayed turn off: %s" % minutes) + + return self.send("s_t_off", [minutes]) + + @command( + click.argument("direction", type=EnumType(MoveDirection)), + default_output=format_output("Rotating the fan to the {direction}"), + ) + def set_rotate(self, direction: MoveDirection): + """Rotate the fan by -5/+5 degrees left/right.""" + return self.send("m_roll", [direction.value]) diff --git a/miio/integrations/fan/dmaker/test_fan.py b/miio/integrations/fan/dmaker/test_fan.py new file mode 100644 index 000000000..aad7cb790 --- /dev/null +++ b/miio/integrations/fan/dmaker/test_fan.py @@ -0,0 +1,190 @@ +from unittest import TestCase + +import pytest + +from miio.fan_common import FanException, OperationMode +from miio.tests.dummies import DummyDevice + +from .fan import MODEL_FAN_P5, FanP5, FanStatusP5 + + +class DummyFanP5(DummyDevice, FanP5): + def __init__(self, *args, **kwargs): + self._model = MODEL_FAN_P5 + self.state = { + "power": True, + "mode": "normal", + "speed": 35, + "roll_enable": False, + "roll_angle": 140, + "time_off": 0, + "light": True, + "beep_sound": False, + "child_lock": False, + } + + self.return_values = { + "get_prop": self._get_state, + "s_power": lambda x: self._set_state("power", x), + "s_mode": lambda x: self._set_state("mode", x), + "s_speed": lambda x: self._set_state("speed", x), + "s_roll": lambda x: self._set_state("roll_enable", x), + "s_angle": lambda x: self._set_state("roll_angle", x), + "s_t_off": lambda x: self._set_state("time_off", x), + "s_light": lambda x: self._set_state("light", x), + "s_sound": lambda x: self._set_state("beep_sound", x), + "s_lock": lambda x: self._set_state("child_lock", x), + } + super().__init__(args, kwargs) + + +@pytest.fixture(scope="class") +def fanp5(request): + request.cls.device = DummyFanP5() + # TODO add ability to test on a real device + + +@pytest.mark.usefixtures("fanp5") +class TestFanP5(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() + + assert repr(self.state()) == repr(FanStatusP5(self.device.start_state)) + + assert self.is_on() is True + assert self.state().mode == OperationMode(self.device.start_state["mode"]) + assert self.state().speed == self.device.start_state["speed"] + assert self.state().oscillate is self.device.start_state["roll_enable"] + assert self.state().angle == self.device.start_state["roll_angle"] + assert self.state().delay_off_countdown == self.device.start_state["time_off"] + assert self.state().led is self.device.start_state["light"] + assert self.state().buzzer is self.device.start_state["beep_sound"] + assert self.state().child_lock is self.device.start_state["child_lock"] + + def test_set_mode(self): + def mode(): + return self.device.status().mode + + self.device.set_mode(OperationMode.Normal) + assert mode() == OperationMode.Normal + + self.device.set_mode(OperationMode.Nature) + assert mode() == OperationMode.Nature + + def test_set_speed(self): + def speed(): + return self.device.status().speed + + self.device.set_speed(0) + assert speed() == 0 + self.device.set_speed(1) + assert speed() == 1 + self.device.set_speed(100) + assert speed() == 100 + + with pytest.raises(FanException): + self.device.set_speed(-1) + + with pytest.raises(FanException): + self.device.set_speed(101) + + def test_set_angle(self): + def angle(): + return self.device.status().angle + + 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 + self.device.set_angle(140) + assert angle() == 140 + + with pytest.raises(FanException): + self.device.set_angle(-1) + + with pytest.raises(FanException): + self.device.set_angle(1) + + with pytest.raises(FanException): + self.device.set_angle(31) + + with pytest.raises(FanException): + self.device.set_angle(141) + + 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(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_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 + self.device.delay_off(0) + assert delay_off_countdown() == 0 + + with pytest.raises(FanException): + self.device.delay_off(-1) diff --git a/miio/integrations/fan/zhimi/__init__.py b/miio/integrations/fan/zhimi/__init__.py index abc5d5da3..0db0cd1d9 100644 --- a/miio/integrations/fan/zhimi/__init__.py +++ b/miio/integrations/fan/zhimi/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa -from .zhimi_miot import FanStatusZA5, FanZA5, OperationModeFanZA5 +from .fan import Fan, FanSA1, FanV2, FanZA1, FanZA4 +from .zhimi_miot import FanZA5 diff --git a/miio/fan.py b/miio/integrations/fan/zhimi/fan.py similarity index 64% rename from miio/fan.py rename to miio/integrations/fan/zhimi/fan.py index 473b8277c..0bf3226e6 100644 --- a/miio/fan.py +++ b/miio/integrations/fan/zhimi/fan.py @@ -3,10 +3,10 @@ import click -from .click_common import EnumType, command, format_output -from .device import Device, DeviceStatus -from .fan_common import FanException, LedBrightness, MoveDirection, OperationMode -from .utils import deprecated +from miio import Device, DeviceStatus +from miio.click_common import EnumType, command, format_output +from miio.fan_common import FanException, LedBrightness, MoveDirection +from miio.utils import deprecated _LOGGER = logging.getLogger(__name__) @@ -16,7 +16,6 @@ MODEL_FAN_ZA1 = "zhimi.fan.za1" MODEL_FAN_ZA3 = "zhimi.fan.za3" MODEL_FAN_ZA4 = "zhimi.fan.za4" -MODEL_FAN_P5 = "dmaker.fan.p5" AVAILABLE_PROPERTIES_COMMON = [ "angle", @@ -41,26 +40,14 @@ "button_pressed", ] + AVAILABLE_PROPERTIES_COMMON -AVAILABLE_PROPERTIES_P5 = [ - "power", - "mode", - "speed", - "roll_enable", - "roll_angle", - "time_off", - "light", - "beep_sound", - "child_lock", -] AVAILABLE_PROPERTIES = { - MODEL_FAN_V2: ["led", "bat_state"] + AVAILABLE_PROPERTIES_COMMON_V2_V3, MODEL_FAN_V3: AVAILABLE_PROPERTIES_COMMON_V2_V3, + MODEL_FAN_V2: ["led", "bat_state"] + AVAILABLE_PROPERTIES_COMMON_V2_V3, MODEL_FAN_SA1: AVAILABLE_PROPERTIES_COMMON, MODEL_FAN_ZA1: AVAILABLE_PROPERTIES_COMMON, MODEL_FAN_ZA3: AVAILABLE_PROPERTIES_COMMON, MODEL_FAN_ZA4: AVAILABLE_PROPERTIES_COMMON, - MODEL_FAN_P5: AVAILABLE_PROPERTIES_P5, } @@ -210,84 +197,10 @@ def button_pressed(self) -> Optional[str]: return None -class FanStatusP5(DeviceStatus): - """Container for status reports from the Xiaomi Mi Smart Pedestal Fan DMaker P5.""" - - def __init__(self, data: Dict[str, Any]) -> None: - """Response of a Fan (dmaker.fan.p5): - - {'power': False, 'mode': 'normal', 'speed': 35, 'roll_enable': False, - 'roll_angle': 140, 'time_off': 0, 'light': True, 'beep_sound': False, - 'child_lock': False} - """ - self.data = data - - @property - def power(self) -> str: - """Power state.""" - return "on" if self.data["power"] else "off" - - @property - def is_on(self) -> bool: - """True if device is currently on.""" - return self.data["power"] - - @property - def mode(self) -> OperationMode: - """Operation mode.""" - return OperationMode(self.data["mode"]) - - @property - def speed(self) -> int: - """Speed of the motor.""" - return self.data["speed"] - - @property - def oscillate(self) -> bool: - """True if oscillation is enabled.""" - return self.data["roll_enable"] - - @property - def angle(self) -> int: - """Oscillation angle.""" - return self.data["roll_angle"] - - @property - def delay_off_countdown(self) -> int: - """Countdown until turning off in seconds.""" - return self.data["time_off"] - - @property - def led(self) -> bool: - """True if LED is turned on, if available.""" - return self.data["light"] - - @property - def buzzer(self) -> bool: - """True if buzzer is turned on.""" - return self.data["beep_sound"] - - @property - def child_lock(self) -> bool: - """True if child lock is on.""" - return self.data["child_lock"] - - class Fan(Device): """Main class representing the Xiaomi Mi Smart Pedestal Fan.""" - _supported_models = list(AVAILABLE_PROPERTIES.keys() - MODEL_FAN_P5) - - 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, model=model) + _supported_models = list(AVAILABLE_PROPERTIES.keys()) @command( default_output=format_output( @@ -523,157 +436,3 @@ def __init__( lazy_discover: bool = True, ) -> None: super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_ZA4) - - -class FanP5(Device): - """Support for dmaker.fan.p5.""" - - _supported_models = [MODEL_FAN_P5] - - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - model: str = MODEL_FAN_P5, - ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover, model=model) - - @command( - default_output=format_output( - "", - "Power: {result.power}\n" - "Operation mode: {result.mode}\n" - "Speed: {result.speed}\n" - "Oscillate: {result.oscillate}\n" - "Angle: {result.angle}\n" - "LED: {result.led}\n" - "Buzzer: {result.buzzer}\n" - "Child lock: {result.child_lock}\n" - "Power-off time: {result.delay_off_countdown}\n", - ) - ) - def status(self) -> FanStatusP5: - """Retrieve properties.""" - properties = AVAILABLE_PROPERTIES[self.model] - values = self.get_properties(properties, max_properties=15) - - return FanStatusP5(dict(zip(properties, values))) - - @command(default_output=format_output("Powering on")) - def on(self): - """Power on.""" - return self.send("s_power", [True]) - - @command(default_output=format_output("Powering off")) - def off(self): - """Power off.""" - return self.send("s_power", [False]) - - @command( - click.argument("mode", type=EnumType(OperationMode)), - default_output=format_output("Setting mode to '{mode.value}'"), - ) - def set_mode(self, mode: OperationMode): - """Set mode.""" - return self.send("s_mode", [mode.value]) - - @command( - click.argument("speed", type=int), - default_output=format_output("Setting speed to {speed}"), - ) - def set_speed(self, speed: int): - """Set speed.""" - if speed < 0 or speed > 100: - raise FanException("Invalid speed: %s" % speed) - - return self.send("s_speed", [speed]) - - @command( - click.argument("angle", type=int), - default_output=format_output("Setting angle to {angle}"), - ) - def set_angle(self, angle: int): - """Set the oscillation angle.""" - if angle not in [30, 60, 90, 120, 140]: - raise FanException( - "Unsupported angle. Supported values: 30, 60, 90, 120, 140" - ) - - return self.send("s_angle", [angle]) - - @command( - click.argument("oscillate", type=bool), - default_output=format_output( - lambda oscillate: "Turning on oscillate" - if oscillate - else "Turning off oscillate" - ), - ) - def set_oscillate(self, oscillate: bool): - """Set oscillate on/off.""" - if oscillate: - return self.send("s_roll", [True]) - else: - return self.send("s_roll", [False]) - - @command( - click.argument("led", type=bool), - default_output=format_output( - lambda led: "Turning on LED" if led else "Turning off LED" - ), - ) - def set_led(self, led: bool): - """Turn led on/off.""" - if led: - return self.send("s_light", [True]) - else: - return self.send("s_light", [False]) - - @command( - click.argument("buzzer", type=bool), - default_output=format_output( - lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer" - ), - ) - def set_buzzer(self, buzzer: bool): - """Set buzzer on/off.""" - if buzzer: - return self.send("s_sound", [True]) - else: - return self.send("s_sound", [False]) - - @command( - click.argument("lock", type=bool), - default_output=format_output( - lambda lock: "Turning on child lock" if lock else "Turning off child lock" - ), - ) - def set_child_lock(self, lock: bool): - """Set child lock on/off.""" - if lock: - return self.send("s_lock", [True]) - else: - return self.send("s_lock", [False]) - - @command( - click.argument("minutes", type=int), - default_output=format_output("Setting delayed turn off to {minutes} minutes"), - ) - def delay_off(self, minutes: int): - """Set delay off minutes.""" - - if minutes < 0: - raise FanException("Invalid value for a delayed turn off: %s" % minutes) - - return self.send("s_t_off", [minutes]) - - @command( - click.argument("direction", type=EnumType(MoveDirection)), - default_output=format_output("Rotating the fan to the {direction}"), - ) - def set_rotate(self, direction: MoveDirection): - """Rotate the fan by -5/+5 degrees left/right.""" - return self.send("m_roll", [direction.value]) diff --git a/miio/tests/test_fan.py b/miio/integrations/fan/zhimi/test_fan.py similarity index 81% rename from miio/tests/test_fan.py rename to miio/integrations/fan/zhimi/test_fan.py index 0ee925b01..0348683b0 100644 --- a/miio/tests/test_fan.py +++ b/miio/integrations/fan/zhimi/test_fan.py @@ -2,22 +2,19 @@ import pytest -from miio import Fan, FanP5 -from miio.fan import ( - MODEL_FAN_P5, +from miio.tests.dummies import DummyDevice + +from .fan import ( MODEL_FAN_SA1, MODEL_FAN_V2, MODEL_FAN_V3, + Fan, FanException, FanStatus, - FanStatusP5, LedBrightness, MoveDirection, - OperationMode, ) -from .dummies import DummyDevice - class DummyFanV2(DummyDevice, Fan): def __init__(self, *args, **kwargs): @@ -741,185 +738,3 @@ def delay_off_countdown(): with pytest.raises(FanException): self.device.delay_off(-1) - - -class DummyFanP5(DummyDevice, FanP5): - def __init__(self, *args, **kwargs): - self._model = MODEL_FAN_P5 - self.state = { - "power": True, - "mode": "normal", - "speed": 35, - "roll_enable": False, - "roll_angle": 140, - "time_off": 0, - "light": True, - "beep_sound": False, - "child_lock": False, - } - - self.return_values = { - "get_prop": self._get_state, - "s_power": lambda x: self._set_state("power", x), - "s_mode": lambda x: self._set_state("mode", x), - "s_speed": lambda x: self._set_state("speed", x), - "s_roll": lambda x: self._set_state("roll_enable", x), - "s_angle": lambda x: self._set_state("roll_angle", x), - "s_t_off": lambda x: self._set_state("time_off", x), - "s_light": lambda x: self._set_state("light", x), - "s_sound": lambda x: self._set_state("beep_sound", x), - "s_lock": lambda x: self._set_state("child_lock", x), - } - super().__init__(args, kwargs) - - -@pytest.fixture(scope="class") -def fanp5(request): - request.cls.device = DummyFanP5() - # TODO add ability to test on a real device - - -@pytest.mark.usefixtures("fanp5") -class TestFanP5(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() - - assert repr(self.state()) == repr(FanStatusP5(self.device.start_state)) - - assert self.is_on() is True - assert self.state().mode == OperationMode(self.device.start_state["mode"]) - assert self.state().speed == self.device.start_state["speed"] - assert self.state().oscillate is self.device.start_state["roll_enable"] - assert self.state().angle == self.device.start_state["roll_angle"] - assert self.state().delay_off_countdown == self.device.start_state["time_off"] - assert self.state().led is self.device.start_state["light"] - assert self.state().buzzer is self.device.start_state["beep_sound"] - assert self.state().child_lock is self.device.start_state["child_lock"] - - def test_set_mode(self): - def mode(): - return self.device.status().mode - - self.device.set_mode(OperationMode.Normal) - assert mode() == OperationMode.Normal - - self.device.set_mode(OperationMode.Nature) - assert mode() == OperationMode.Nature - - def test_set_speed(self): - def speed(): - return self.device.status().speed - - self.device.set_speed(0) - assert speed() == 0 - self.device.set_speed(1) - assert speed() == 1 - self.device.set_speed(100) - assert speed() == 100 - - with pytest.raises(FanException): - self.device.set_speed(-1) - - with pytest.raises(FanException): - self.device.set_speed(101) - - def test_set_angle(self): - def angle(): - return self.device.status().angle - - 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 - self.device.set_angle(140) - assert angle() == 140 - - with pytest.raises(FanException): - self.device.set_angle(-1) - - with pytest.raises(FanException): - self.device.set_angle(1) - - with pytest.raises(FanException): - self.device.set_angle(31) - - with pytest.raises(FanException): - self.device.set_angle(141) - - 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(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_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 - self.device.delay_off(0) - assert delay_off_countdown() == 0 - - with pytest.raises(FanException): - self.device.delay_off(-1)