diff --git a/miio/discovery.py b/miio/discovery.py index 3138f7860..e8869d21d 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -12,6 +12,7 @@ WaterPurifier, WifiSpeaker, WifiRepeater, Yeelight, ) from .chuangmi_plug import (MODEL_CHUANGMI_PLUG_V1, MODEL_CHUANGMI_PLUG_V3, MODEL_CHUANGMI_PLUG_M1, ) +from .powerstrip import (MODEL_POWER_STRIP_V1, MODEL_POWER_STRIP_V2, ) _LOGGER = logging.getLogger(__name__) @@ -24,8 +25,8 @@ "chuangmi-plug-v1": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_V1), "chuangmi-plug_": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_V1), "chuangmi-plug-v3": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_V3), - "qmi-powerstrip-v1": PowerStrip, - "zimi-powerstrip-v2": PowerStrip, + "qmi-powerstrip-v1": partial(PowerStrip, model=MODEL_POWER_STRIP_V1), + "zimi-powerstrip-v2": partial(PowerStrip, model=MODEL_POWER_STRIP_V2), "zhimi-airpurifier-m1": AirPurifier, # mini model "zhimi-airpurifier-m2": AirPurifier, # mini model 2 "zhimi-airpurifier-ma1": AirPurifier, # ms model diff --git a/miio/powerstrip.py b/miio/powerstrip.py index d0aef0220..b716eab24 100644 --- a/miio/powerstrip.py +++ b/miio/powerstrip.py @@ -10,6 +10,31 @@ _LOGGER = logging.getLogger(__name__) +MODEL_POWER_STRIP_V1 = 'qmi.powerstrip.v1' +MODEL_POWER_STRIP_V2 = 'zimi.powerstrip.v2' + +AVAILABLE_PROPERTIES = { + MODEL_POWER_STRIP_V1: [ + 'power', + 'temperature', + 'current', + 'mode', + 'power_consume_rate', + 'voltage', + 'power_factor', + 'elec_leakage', + ], + MODEL_POWER_STRIP_V2: [ + 'power', + 'temperature', + 'current', + 'mode', + 'power_consume_rate', + 'wifi_led', + 'power_price', + ], +} + class PowerStripException(DeviceException): pass @@ -70,35 +95,37 @@ def mode(self) -> Optional[PowerMode]: return None @property - def wifi_led(self) -> bool: + def wifi_led(self) -> Optional[bool]: """True if the wifi led is turned on.""" - return self.data["wifi_led"] == "on" + if "wifi_led" in self.data and self.data["wifi_led"] is not None: + return self.data["wifi_led"] == "on" + return None @property def power_price(self) -> Optional[int]: """The stored power price, if available.""" - if self.data["power_price"] is not None: + if "power_price" in self.data and self.data["power_price"] is not None: return self.data["power_price"] return None @property def leakage_current(self) -> Optional[int]: """The leakage current, if available.""" - if self.data["elec_leakage"] is not None: + if "elec_leakage" in self.data and self.data["elec_leakage"] is not None: return self.data["elec_leakage"] return None @property - def voltage(self) -> Optional[int]: + def voltage(self) -> Optional[float]: """The voltage, if available.""" - if self.data["voltage"] is not None: - return self.data["voltage"] + if "voltage" in self.data and self.data["voltage"] is not None: + return self.data["voltage"] / 100.0 return None @property def power_factor(self) -> Optional[float]: """The power factor, if available.""" - if self.data["power_factor"] is not None: + if "power_factor" in self.data and self.data["power_factor"] is not None: return self.data["power_factor"] return None @@ -132,6 +159,16 @@ def __json__(self): class PowerStrip(Device): """Main class representing the smart power strip.""" + def __init__(self, ip: str = None, token: str = None, start_id: int = 0, + debug: int = 0, lazy_discover: bool = True, + model: str = MODEL_POWER_STRIP_V1) -> None: + super().__init__(ip, token, start_id, debug, lazy_discover) + + if model in AVAILABLE_PROPERTIES: + self.model = model + else: + self.model = MODEL_POWER_STRIP_V1 + @command( default_output=format_output( "", @@ -148,9 +185,7 @@ class PowerStrip(Device): ) def status(self) -> PowerStripStatus: """Retrieve properties.""" - properties = ['power', 'temperature', 'current', 'mode', - 'power_consume_rate', 'wifi_led', 'power_price', - 'voltage', 'power_factor', 'elec_leakage'] + properties = AVAILABLE_PROPERTIES[self.model] values = self.send( "get_prop", properties diff --git a/miio/tests/test_powerstrip.py b/miio/tests/test_powerstrip.py index faabe5112..a78789336 100644 --- a/miio/tests/test_powerstrip.py +++ b/miio/tests/test_powerstrip.py @@ -3,12 +3,106 @@ import pytest from miio import PowerStrip -from miio.powerstrip import PowerMode, PowerStripStatus, PowerStripException +from miio.powerstrip import (PowerMode, PowerStripStatus, PowerStripException, + MODEL_POWER_STRIP_V1, MODEL_POWER_STRIP_V2, ) from .dummies import DummyDevice -class DummyPowerStrip(DummyDevice, PowerStrip): +class DummyPowerStripV1(DummyDevice, PowerStrip): def __init__(self, *args, **kwargs): + self.model = MODEL_POWER_STRIP_V1 + self.state = { + 'power': 'on', + 'mode': 'normal', + 'temperature': 32.5, + 'current': 25.5, + 'power_consume_rate': 12.5, + 'voltage': 23057, + 'power_factor': 12, + 'elec_leakage': 8, + } + self.return_values = { + 'get_prop': self._get_state, + 'set_power': lambda x: self._set_state("power", x), + 'set_power_mode': lambda x: self._set_state("mode", x), + } + super().__init__(args, kwargs) + + +@pytest.fixture(scope="class") +def powerstripv1(request): + request.cls.device = DummyPowerStripV1() + # TODO add ability to test on a real device + + +@pytest.mark.usefixtures("powerstripv1") +class TestPowerStripV1(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(PowerStripStatus(self.device.start_state)) + + assert self.is_on() is True + assert self.state().mode == PowerMode(self.device.start_state["mode"]) + assert self.state().temperature == self.device.start_state["temperature"] + assert self.state().current == self.device.start_state["current"] + assert self.state().load_power == self.device.start_state["power_consume_rate"] + assert self.state().voltage == self.device.start_state["voltage"] / 100.0 + assert self.state().power_factor == self.device.start_state["power_factor"] + assert self.state().leakage_current == self.device.start_state["elec_leakage"] + + def test_status_without_power_consume_rate(self): + self.device._reset_state() + + self.device.state["power_consume_rate"] = None + assert self.state().load_power is None + + def test_status_without_current(self): + self.device._reset_state() + + self.device.state["current"] = None + assert self.state().current is None + + def test_status_without_mode(self): + self.device._reset_state() + + # The Power Strip 2 doesn't support power modes + self.device.state["mode"] = None + assert self.state().mode is None + + def test_set_power_mode(self): + def mode(): + return self.device.status().mode + + self.device.set_power_mode(PowerMode.Eco) + assert mode() == PowerMode.Eco + self.device.set_power_mode(PowerMode.Normal) + assert mode() == PowerMode.Normal + + +class DummyPowerStripV2(DummyDevice, PowerStrip): + def __init__(self, *args, **kwargs): + self.model = MODEL_POWER_STRIP_V2 self.state = { 'power': 'on', 'mode': 'normal', @@ -17,9 +111,6 @@ def __init__(self, *args, **kwargs): 'power_consume_rate': 12.5, 'wifi_led': 'off', 'power_price': 49, - 'voltage': 230, - 'elec_leakage': 0, - 'power_factor': 0.5, } self.return_values = { 'get_prop': self._get_state, @@ -33,13 +124,13 @@ def __init__(self, *args, **kwargs): @pytest.fixture(scope="class") -def powerstrip(request): - request.cls.device = DummyPowerStrip() +def powerstripv2(request): + request.cls.device = DummyPowerStripV2() # TODO add ability to test on a real device -@pytest.mark.usefixtures("powerstrip") -class TestPowerStrip(TestCase): +@pytest.mark.usefixtures("powerstripv2") +class TestPowerStripV2(TestCase): def is_on(self): return self.device.status().is_on @@ -70,9 +161,9 @@ def test_status(self): assert self.state().temperature == self.device.start_state["temperature"] assert self.state().current == self.device.start_state["current"] assert self.state().load_power == self.device.start_state["power_consume_rate"] - assert self.state().voltage == self.device.start_state["voltage"] - assert self.state().power_factor == self.device.start_state["power_factor"] - assert self.state().leakage_current == self.device.start_state["elec_leakage"] + assert self.state().voltage is None + assert self.state().power_factor is None + assert self.state().leakage_current is None def test_status_without_power_consume_rate(self): self.device._reset_state()