Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xiaomi Power Strip V1 is unable to handle some V2 properties #303

Merged
merged 5 commits into from
Apr 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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
Expand Down
57 changes: 46 additions & 11 deletions miio/powerstrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(
"",
Expand All @@ -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
Expand Down
115 changes: 103 additions & 12 deletions miio/tests/test_powerstrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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()
Expand Down