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

Add support for the cgllc.airmonitor.b1 #562

Merged
merged 16 commits into from
Oct 20, 2019
Merged
86 changes: 46 additions & 40 deletions miio/airqualitymonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@
"sensor_state",
]

AVAILABLE_PROPERTIES_B1 = [
"co2e",
"humidity",
"pm25",
"temperature",
"tvoc"
]

AVAILABLE_PROPERTIES_S1 = ["battery", "co2", "humidity", "pm25", "temperature", "tvoc"]

AVAILABLE_PROPERTIES = {
MODEL_AIRQUALITYMONITOR_V1: AVAILABLE_PROPERTIES_COMMON,
MODEL_AIRQUALITYMONITOR_B1: AVAILABLE_PROPERTIES_COMMON,
MODEL_AIRQUALITYMONITOR_B1: AVAILABLE_PROPERTIES_B1,
MODEL_AIRQUALITYMONITOR_S1: AVAILABLE_PROPERTIES_S1,
}

Expand All @@ -49,7 +57,8 @@ def __init__(self, data):

Response of a Xiaomi Air Quality Monitor (cgllc.airmonitor.b1):

unknown.
{'co2e': 1466, 'humidity': 59.79999923706055, 'pm25': 2, 'temperature': 19.799999237060547,
'temperature_unit': 'c', 'tvoc': 1.3948699235916138, 'tvoc_unit': 'mg_m3'}

Response of a Xiaomi Air Quality Monitor (cgllc.airmonitor.s1):

Expand All @@ -61,9 +70,7 @@ def __init__(self, data):
@property
def power(self) -> Optional[str]:
"""Current power state."""
if "power" in self.data and self.data["power"] is not None:
return self.data["power"]
return None
return self.data.get("power", None)

@property
def is_on(self) -> bool:
Expand All @@ -80,14 +87,12 @@ def usb_power(self) -> Optional[bool]:
@property
def aqi(self) -> Optional[int]:
"""Air quality index value. (0...600)."""
if "aqi" in self.data and self.data["aqi"] is not None:
return self.data["aqi"]
return None
return self.data.get("aqi", None)

@property
def battery(self) -> int:
def battery(self) -> Optional[int]:
"""Current battery level (0...100)."""
return self.data["battery"]
return self.data.get("battery", None)

@property
def display_clock(self) -> Optional[bool]:
Expand All @@ -106,58 +111,47 @@ def night_mode(self) -> Optional[bool]:
@property
def night_time_begin(self) -> Optional[str]:
"""Return the begin of the night time."""
if "night_beg_time" in self.data and self.data["night_beg_time"] is not None:
return self.data["night_beg_time"]
return None
return self.data.get("night_beg_time", None)

@property
def night_time_end(self) -> Optional[str]:
"""Return the end of the night time."""
if "night_end_time" in self.data and self.data["night_end_time"] is not None:
return self.data["night_end_time"]
return None
return self.data.get("night_end_time", None)

@property
def sensor_state(self) -> Optional[str]:
"""Sensor state."""
if "sensor_state" in self.data and self.data["sensor_state"] is not None:
return self.data["sensor_state"]
return None
return self.data.get("sensor_state", None)

@property
def co2(self) -> Optional[int]:
"""Return co2 value (400...9999ppm)."""
if "co2" in self.data and self.data["co2"] is not None:
return self.data["co2"]
return None
return self.data.get("co2", None)

@property
def co2e(self) -> Optional[int]:
"""Return co2e value (400...9999ppm)."""
return self.data.get("co2e", None)

@property
def humidity(self) -> Optional[float]:
"""Return humidity value (0...100%)."""
if "humidity" in self.data and self.data["humidity"] is not None:
return self.data["humidity"]
return None
return self.data.get("humidity", None)

@property
def pm25(self) -> Optional[float]:
"""Return pm2.5 value (0...999μg/m³)."""
if "pm25" in self.data and self.data["pm25"] is not None:
return self.data["pm25"]
return None
return self.data.get("pm25", None)

@property
def temperature(self) -> Optional[float]:
"""Return temperature value (-10...50°C)."""
if "temperature" in self.data and self.data["temperature"] is not None:
return self.data["temperature"]
return None
return self.data.get("temperature", None)

@property
def tvoc(self) -> Optional[int]:
"""Return tvoc value."""
if "tvoc" in self.data and self.data["tvoc"] is not None:
return self.data["tvoc"]
return None
return self.data.get("tvoc", None)

def __repr__(self) -> str:
s = (
Expand All @@ -168,6 +162,7 @@ def __repr__(self) -> str:
"temperature=%s, "
"humidity=%s, "
"co2=%s, "
"co2e=%s, "
"pm2.5=%s, "
"tvoc=%s, "
"display_clock=%s>"
Expand All @@ -179,6 +174,7 @@ def __repr__(self) -> str:
self.temperature,
self.humidity,
self.co2,
self.co2e,
self.pm25,
self.tvoc,
self.display_clock,
Expand Down Expand Up @@ -206,11 +202,12 @@ def __init__(

if model in AVAILABLE_PROPERTIES:
self.model = model
else:
elif model is not None:
self.model = MODEL_AIRQUALITYMONITOR_V1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a model is provided, but not in available_properties fallback is still there.

_LOGGER.error(
"Device model %s unsupported. Falling back to %s.", model, self.model
)
_LOGGER.error("Device model %s unsupported. Falling back to %s.", model, self.model)
else:
"""Force autodetection"""
self.model = None

@command(
default_output=format_output(
Expand All @@ -222,6 +219,7 @@ def __init__(
"Temperature: {result.temperature}\n"
"Humidity: {result.humidity}\n"
"CO2: {result.co2}\n"
"CO2e: {result.co2e}\n"
"PM2.5: {result.pm25}\n"
"TVOC: {result.tvoc}\n"
"Display clock: {result.display_clock}\n",
Expand All @@ -230,9 +228,17 @@ def __init__(
def status(self) -> AirQualityMonitorStatus:
"""Return device status."""

if self.model is None:
"""Autodetection"""
info = self.info()
self.model = info.model

properties = AVAILABLE_PROPERTIES[self.model]

values = self.send("get_prop", properties)
if self.model == MODEL_AIRQUALITYMONITOR_B1:
values = self.send("get_air_data")
else:
values = self.send("get_prop", properties)

properties_count = len(properties)
values_count = len(values)
Expand All @@ -244,7 +250,7 @@ def status(self) -> AirQualityMonitorStatus:
values_count,
)

if self.model == MODEL_AIRQUALITYMONITOR_S1:
if self.model == MODEL_AIRQUALITYMONITOR_S1 or self.model == MODEL_AIRQUALITYMONITOR_B1:
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
return AirQualityMonitorStatus(defaultdict(lambda: None, values))
else:
return AirQualityMonitorStatus(
Expand Down
52 changes: 52 additions & 0 deletions miio/tests/test_airqualitymonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
AirQualityMonitorStatus,
MODEL_AIRQUALITYMONITOR_V1,
MODEL_AIRQUALITYMONITOR_S1,
MODEL_AIRQUALITYMONITOR_B1,
)
from .dummies import DummyDevice

Expand Down Expand Up @@ -128,3 +129,54 @@ def test_status(self):
assert self.state().usb_power is None
assert self.state().display_clock is None
assert self.state().night_mode is None


class DummyAirQualityMonitorB1(DummyDevice, AirQualityMonitor):
def __init__(self, *args, **kwargs):
self.model = MODEL_AIRQUALITYMONITOR_B1
self.state = {
"co2e": 1466,
"humidity": 59.79999923706055,
"pm25": 2,
"temperature": 19.799999237060547,
"temperature_unit": "c",
"tvoc": 1.3948699235916138,
"tvoc_unit": "mg_m3"}
self.return_values = {"get_air_data": self._get_state}
super().__init__(args, kwargs)

def _get_state(self, props):
"""Return wanted properties"""
return self.state


@pytest.fixture(scope="class")
def airqualitymonitorb1(request):
request.cls.device = DummyAirQualityMonitorB1()
# TODO add ability to test on a real device


@pytest.mark.usefixtures("airqualitymonitorb1")
class TestAirQualityMonitorB1(TestCase):
def state(self):
return self.device.status()

def test_status(self):
self.device._reset_state()

assert repr(self.state()) == repr(
AirQualityMonitorStatus(self.device.start_state)
)

assert self.state().power is None
assert self.state().usb_power is None
assert self.state().battery is None
assert self.state().aqi is None
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().co2 is None
assert self.state().co2e == self.device.start_state["co2e"]
assert self.state().pm25 == self.device.start_state["pm25"]
assert self.state().tvoc == self.device.start_state["tvoc"]
assert self.state().display_clock is None
assert self.state().night_mode is None
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved