From 33f0a73ccefefa0a24bb06e21ee8f4a71af7ddeb Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 25 Jul 2018 20:39:16 +0200 Subject: [PATCH 01/12] Add zimi.fan.sa1 support --- miio/fan.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index 3b18a03d6..7d8ff2ca5 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -11,6 +11,7 @@ MODEL_FAN_V2 = 'zimi.fan.v2' MODEL_FAN_V3 = 'zimi.fan.v3' +MODEL_FAN_SA1 = 'zimi.fan.sa1' AVAILABLE_PROPERTIES_COMMON = [ 'temp_dec', @@ -35,6 +36,8 @@ AVAILABLE_PROPERTIES = { MODEL_FAN_V2: ['led', 'bat_state'] + AVAILABLE_PROPERTIES_COMMON, MODEL_FAN_V3: AVAILABLE_PROPERTIES_COMMON, + MODEL_FAN_SA1: ['led', 'angle', 'speed', 'poweroff_time', 'power', 'ac_power', 'angle_enable', 'speed_level', + 'natural_level', 'child_lock', 'buzzer', 'led_b', 'use_time'], } @@ -66,6 +69,11 @@ def __init__(self, data: Dict[str, Any]) -> None: 'child_lock': 'off', 'buzzer': 'on', 'led_b': 1, 'led': None, 'natural_enable': None, 'use_time': 0, 'bat_charge': 'complete', 'bat_state': None, 'button_pressed':'speed'} + + Response of a Fan (zhimi.fan.sa1): + {'led': 0, 'angle': 120, 'speed': 277, 'poweroff_time': 0, 'power': 'on', + 'ac_power': 'on', 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 2, + 'child_lock': 'off', 'buzzer': 0, 'led_b': 0, 'use_time': 2318} """ self.data = data @@ -80,14 +88,18 @@ def is_on(self) -> bool: return self.power == "on" @property - def humidity(self) -> int: + def humidity(self) -> Optional[int]: """Current humidity.""" - return self.data["humidity"] + if "humidity" in self.data and self.data["humidity"] is not None: + return self.data["humidity"] + return None @property - def temperature(self) -> float: + def temperature(self) -> Optional[float]: """Current temperature, if available.""" - return self.data["temp_dec"] / 10.0 + if "temp_dec" in self.data and self.data["temp_dec"] is not None: + return self.data["temp_dec"] / 10.0 + return None @property def led(self) -> Optional[bool]: @@ -129,14 +141,15 @@ def oscillate(self) -> bool: return self.data["angle_enable"] == "on" @property - def battery(self) -> int: + def battery(self) -> Optional[int]: """Current battery level.""" - return self.data["battery"] + if "battery" in self.data and self.data["battery"] is not None: + 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: + if "bat_charge" in self.data and self.data["bat_charge"] is not None: return self.data["bat_charge"] return None From aa95ee77e075af1b72466ada1cd4ad3e68c40b9f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 25 Jul 2018 20:41:55 +0200 Subject: [PATCH 02/12] Make hound happy --- miio/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index 7d8ff2ca5..d29b9254d 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -36,8 +36,8 @@ AVAILABLE_PROPERTIES = { MODEL_FAN_V2: ['led', 'bat_state'] + AVAILABLE_PROPERTIES_COMMON, MODEL_FAN_V3: AVAILABLE_PROPERTIES_COMMON, - MODEL_FAN_SA1: ['led', 'angle', 'speed', 'poweroff_time', 'power', 'ac_power', 'angle_enable', 'speed_level', - 'natural_level', 'child_lock', 'buzzer', 'led_b', 'use_time'], + MODEL_FAN_SA1: ['led', 'angle', 'speed', 'poweroff_time', 'power', 'ac_power', 'angle_enable', + 'speed_level', 'natural_level', 'child_lock', 'buzzer', 'led_b', 'use_time'], } From c3c1258f1cf35091d502f0eb958fb12fc8fb4ad0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 26 Jul 2018 07:58:21 +0200 Subject: [PATCH 03/12] Add tests --- miio/tests/test_fan.py | 211 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/miio/tests/test_fan.py b/miio/tests/test_fan.py index 2f256ed6d..cdc1e01b0 100644 --- a/miio/tests/test_fan.py +++ b/miio/tests/test_fan.py @@ -4,7 +4,7 @@ from miio import Fan from miio.fan import (MoveDirection, LedBrightness, FanStatus, FanException, - MODEL_FAN_V2, MODEL_FAN_V3, ) + MODEL_FAN_V2, MODEL_FAN_V3, MODEL_FAN_SA1) from .dummies import DummyDevice @@ -490,3 +490,212 @@ def delay_off_countdown(): with pytest.raises(FanException): self.device.delay_off(0) + + +class DummyFanSA1(DummyDevice, Fan): + def __init__(self, *args, **kwargs): + self.model = MODEL_FAN_SA1 + self.state = { + 'led': 0, + 'angle': 120, + 'speed': 277, + 'poweroff_time': 0, + 'power': 'on', + 'ac_power': 'on', + 'angle_enable': 'off', + 'speed_level': 1, + 'natural_level': 2, + 'child_lock': 'off', + 'buzzer': 0, + 'led_b': 0, + 'use_time': 2318 + } + + 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) + + +@pytest.fixture(scope="class") +def fansa1(request): + request.cls.device = DummyFanSA1() + # TODO add ability to test on a real device + + +@pytest.mark.usefixtures("fansa1") +class TestFanSA1(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(FanStatus(self.device.start_state)) + + assert self.is_on() is True + assert self.state().angle == self.device.start_state["angle"] + assert self.state().speed == self.device.start_state["speed"] + assert self.state().delay_off_countdown == self.device.start_state["poweroff_time"] + assert self.state().ac_power is (self.device.start_state["ac_power"] == 'on') + assert self.state().oscillate is (self.device.start_state["angle_enable"] == 'on') + assert self.state().direct_speed == self.device.start_state["speed_level"] + assert self.state().natural_speed == self.device.start_state["natural_level"] + 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().use_time == self.device.start_state["use_time"] + + 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_natural_speed(self): + def natural_speed(): + return self.device.status().natural_speed + + self.device.set_natural_speed(0) + assert natural_speed() == 0 + self.device.set_natural_speed(1) + assert natural_speed() == 1 + self.device.set_natural_speed(100) + assert natural_speed() == 100 + + with pytest.raises(FanException): + self.device.set_natural_speed(-1) + + with pytest.raises(FanException): + self.device.set_natural_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_angle(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 4ed0aa6a635b75c938a5988e6f1ee0866163254f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 3 Aug 2018 09:45:17 +0200 Subject: [PATCH 04/12] Make specific fan classes available by miiocli --- miio/__init__.py | 2 +- miio/fan.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/miio/__init__.py b/miio/__init__.py index a60148c7e..07d65e018 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -8,7 +8,7 @@ from miio.chuangmi_plug import (Plug, PlugV1, PlugV3, ChuangmiPlug) from miio.cooker import Cooker from miio.device import Device, DeviceException -from miio.fan import Fan +from miio.fan import (Fan, FanV2, FanSA1) from miio.philips_bulb import PhilipsBulb from miio.philips_eyecare import PhilipsEyecare from miio.powerstrip import PowerStrip diff --git a/miio/fan.py b/miio/fan.py index d29b9254d..e1c72ab5f 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -428,3 +428,17 @@ def delay_off(self, seconds: int): "Invalid value for a delayed turn off: %s" % seconds) return self.send("set_poweroff_time", [seconds]) + + +class FanV2(Fan): + def __init__(self, ip: str = None, token: str = None, start_id: int = 0, + debug: int = 0, lazy_discover: bool = True) -> None: + super().__init__(ip, token, start_id, debug, lazy_discover, + model=MODEL_FAN_V2) + + +class FanSA1(Fan): + def __init__(self, ip: str = None, token: str = None, start_id: int = 0, + debug: int = 0, lazy_discover: bool = True) -> None: + super().__init__(ip, token, start_id, debug, lazy_discover, + model=MODEL_FAN_SA1) From 7e9300f8e447304e12303e2972ff9593d7039955 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 9 Aug 2018 21:01:06 +0200 Subject: [PATCH 05/12] Fix available properties, set_buzzer and set_oscillate --- miio/fan.py | 18 ++++++++++++------ miio/tests/test_fan.py | 4 +--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index e1c72ab5f..61223d6e4 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -36,7 +36,7 @@ AVAILABLE_PROPERTIES = { MODEL_FAN_V2: ['led', 'bat_state'] + AVAILABLE_PROPERTIES_COMMON, MODEL_FAN_V3: AVAILABLE_PROPERTIES_COMMON, - MODEL_FAN_SA1: ['led', 'angle', 'speed', 'poweroff_time', 'power', 'ac_power', 'angle_enable', + MODEL_FAN_SA1: ['angle', 'speed', 'poweroff_time', 'power', 'ac_power', 'angle_enable', 'speed_level', 'natural_level', 'child_lock', 'buzzer', 'led_b', 'use_time'], } @@ -71,7 +71,7 @@ def __init__(self, data: Dict[str, Any]) -> None: 'bat_state': None, 'button_pressed':'speed'} Response of a Fan (zhimi.fan.sa1): - {'led': 0, 'angle': 120, 'speed': 277, 'poweroff_time': 0, 'power': 'on', + {'angle': 120, 'speed': 277, 'poweroff_time': 0, 'power': 'on', 'ac_power': 'on', 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 2, 'child_lock': 'off', 'buzzer': 0, 'led_b': 0, 'use_time': 2318} """ @@ -118,7 +118,7 @@ def led_brightness(self) -> Optional[LedBrightness]: @property def buzzer(self) -> bool: """True if buzzer is turned on.""" - return self.data["buzzer"] == "on" + return self.data["buzzer"] in ["on", 1, 2] @property def child_lock(self) -> bool: @@ -353,8 +353,8 @@ def set_angle(self, angle: int): @command( click.argument("oscillate", type=bool), default_output=format_output( - lambda lock: "Turning on oscillate" - if lock else "Turning off oscillate" + lambda oscillate: "Turning on oscillate" + if oscillate else "Turning off oscillate" ) ) def set_oscillate(self, oscillate: bool): @@ -381,7 +381,7 @@ def set_led_brightness(self, brightness: LedBrightness): ) ) def set_led(self, led: bool): - """Turn led on/off.""" + """Turn led on/off. Not supported by model SA1.""" if led: return self.send("set_led", ['on']) else: @@ -396,6 +396,12 @@ def set_led(self, led: bool): ) def set_buzzer(self, buzzer: bool): """Set buzzer on/off.""" + if MODEL_FAN_SA1: + if buzzer: + return self.send("set_buzzer", [2]) + else: + return self.send("set_buzzer", [0]) + if buzzer: return self.send("set_buzzer", ["on"]) else: diff --git a/miio/tests/test_fan.py b/miio/tests/test_fan.py index cdc1e01b0..61c36159c 100644 --- a/miio/tests/test_fan.py +++ b/miio/tests/test_fan.py @@ -496,7 +496,6 @@ class DummyFanSA1(DummyDevice, Fan): def __init__(self, *args, **kwargs): self.model = MODEL_FAN_SA1 self.state = { - 'led': 0, 'angle': 120, 'speed': 277, 'poweroff_time': 0, @@ -520,7 +519,6 @@ def __init__(self, *args, **kwargs): '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), @@ -572,7 +570,7 @@ 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"] def test_set_direct_speed(self): From d92f0f59b041b6a50331457de786090b14414abb Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 10 Aug 2018 15:46:32 +0200 Subject: [PATCH 06/12] Limit the number of properties per request --- miio/fan.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index 61223d6e4..6086e4c67 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -276,11 +276,17 @@ def status(self) -> FanStatus: # A single request is limited to 16 properties. Therefore the # properties are divided into multiple requests + _props_per_request = 15 + + # The SA1 is limited to a single property per request + if self.model == MODEL_FAN_SA1: + _props_per_request = 1 + _props = properties.copy() values = [] while _props: - values.extend(self.send("get_prop", _props[:15])) - _props[:] = _props[15:] + values.extend(self.send("get_prop", _props[:_props_per_request])) + _props[:] = _props[_props_per_request:] properties_count = len(properties) values_count = len(values) @@ -396,7 +402,7 @@ def set_led(self, led: bool): ) def set_buzzer(self, buzzer: bool): """Set buzzer on/off.""" - if MODEL_FAN_SA1: + if self.model == MODEL_FAN_SA1: if buzzer: return self.send("set_buzzer", [2]) else: From 8384e7b626b2d0aae256c2ac56ab329a0f0245d0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 10 Aug 2018 17:24:53 +0200 Subject: [PATCH 07/12] Make natural and direct level optional --- miio/fan.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index 6086e4c67..82fc508b2 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -126,14 +126,16 @@ def child_lock(self) -> bool: return self.data["child_lock"] == "on" @property - def natural_speed(self) -> int: + def natural_speed(self) -> Optional[int]: """Speed level in natural mode.""" - return self.data["natural_level"] + if "natural_level" in self.data and self.data["natural_level"] is not None: + return self.data["natural_level"] @property - def direct_speed(self) -> int: + def direct_speed(self) -> Optional[int]: """Speed level in direct mode.""" - return self.data["speed_level"] + if "speed_level" in self.data and self.data["speed_level"] is not None: + return self.data["speed_level"] @property def oscillate(self) -> bool: From ab3a5d1466f4cc7a8c0e18d3355ae4541136fb6d Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 11 Aug 2018 05:07:13 +0200 Subject: [PATCH 08/12] Fix CLI output --- miio/fan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/miio/fan.py b/miio/fan.py index 82fc508b2..4e25ff9fd 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -264,11 +264,11 @@ def __init__(self, ip: str = None, token: str = None, start_id: int = 0, "LED brightness: {result.led_brightness}\n" "Buzzer: {result.buzzer}\n" "Child lock: {result.child_lock}\n" - "Natural level: {result.natural_level}\n" - "Speed level: {result.speed_level}\n" + "Speed: {result.speed}\n" + "Natural speed: {result.natural_speed}\n" + "Direct speed: {result.direct_speed}\n" "Oscillate: {result.oscillate}\n" "Power-off time: {result.poweroff_time}\n" - "Speed: {result.speed}\n" "Angle: {result.angle}\n" ) ) From 9e18d3cbf2d7895659e8b8b4a39771ea1a2da19b Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 11 Aug 2018 11:07:56 +0200 Subject: [PATCH 09/12] Fix another CLI issue --- miio/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/fan.py b/miio/fan.py index 4e25ff9fd..9f75d93fe 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -268,7 +268,7 @@ def __init__(self, ip: str = None, token: str = None, start_id: int = 0, "Natural speed: {result.natural_speed}\n" "Direct speed: {result.direct_speed}\n" "Oscillate: {result.oscillate}\n" - "Power-off time: {result.poweroff_time}\n" + "Power-off time: {result.delay_off_countdown(self)}\n" "Angle: {result.angle}\n" ) ) From b537e4837fba726e9903dc1539878907c2d3b463 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 11 Aug 2018 20:18:46 +0200 Subject: [PATCH 10/12] Fix copy&paste error --- miio/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/fan.py b/miio/fan.py index 9f75d93fe..474ddabc6 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -268,7 +268,7 @@ def __init__(self, ip: str = None, token: str = None, start_id: int = 0, "Natural speed: {result.natural_speed}\n" "Direct speed: {result.direct_speed}\n" "Oscillate: {result.oscillate}\n" - "Power-off time: {result.delay_off_countdown(self)}\n" + "Power-off time: {result.delay_off_countdown}\n" "Angle: {result.angle}\n" ) ) From e4d4255de474b83a108d6846896ad1f97ab5dc6a Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 11 Aug 2018 20:21:01 +0200 Subject: [PATCH 11/12] Update README --- README.rst | 2 +- miio/discovery.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8e04ad395..8df828292 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 Pedestal Fan (:class:`miio.fan`) +- Xiaomi Mi Smart Pedestal Fan V2, V3 and SA1 (: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`) diff --git a/miio/discovery.py b/miio/discovery.py index f159dab84..631e28a43 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -67,6 +67,7 @@ "yeelink-light-": Yeelight, "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), "lumi-gateway-": lambda x: other_package_info( x, "https://github.com/Danielhiversen/PyXiaomiGateway") } # type: Dict[str, Union[Callable, Device]] From 7f3317feceb31be8cb34c17a5d6a848a4cb515f1 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 11 Aug 2018 20:22:06 +0200 Subject: [PATCH 12/12] Add missing import --- miio/discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/discovery.py b/miio/discovery.py index 631e28a43..fa60784f9 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -14,7 +14,7 @@ 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 .fan import (MODEL_FAN_V2, MODEL_FAN_V3, MODEL_FAN_SA1, ) from .powerstrip import (MODEL_POWER_STRIP_V1, MODEL_POWER_STRIP_V2, ) _LOGGER = logging.getLogger(__name__)