Skip to content

Commit

Permalink
Air Purifier Pro second motor speed (#176)
Browse files Browse the repository at this point in the history
* Air Purifier Pro: second motor speed
* Air Purifier Pro: add rfid_* and act_sleep properties add reset_filter method
* Air Purifier: automate get_prop splitting into multiple requests
  • Loading branch information
yawor authored and syssi committed Jan 27, 2018
1 parent 81a834b commit 18d422f
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 23 deletions.
124 changes: 102 additions & 22 deletions miio/airpurifier.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import enum
import re
from typing import Any, Dict, Optional
from collections import defaultdict
from .device import Device, DeviceException
Expand All @@ -24,28 +25,48 @@ class LedBrightness(enum.Enum):
Off = 2


class FilterType(enum.Enum):
Regular = 'regular'
AntiBacterial = 'anti-bacterial'
AntiFormaldehyde = 'anti-formaldehyde'
Unknown = 'unknown'


FILTER_TYPE_RE = (
(re.compile(r'^\d+:\d+:41:30$'), FilterType.AntiBacterial),
(re.compile(r'^\d+:\d+:(30|0|00):31$'), FilterType.AntiFormaldehyde),
(re.compile(r'.*'), FilterType.Regular),
)


class AirPurifierStatus:
"""Container for status reports from the air purifier."""

_filter_type_cache = {}

def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Air Purifier Pro (zhimi.airpurifier.v6):
{'power': 'off', 'aqi': 7, 'average_aqi': 18, 'humidity': 45,
'temp_dec': 234, 'mode': 'auto', 'favorite_level': 17,
'filter1_life': 52, 'f1_hour_used': 1664, 'use_time': 2642700,
'motor1_speed': 0, 'purify_volume': 62180, 'f1_hour': 3500,
'led': 'on', 'led_b': None, 'bright': 83, 'buzzer': None,
'child_lock': 'off', 'volume': 50}
'motor1_speed': 0, 'motor2_speed': 800, 'purify_volume': 62180,
'f1_hour': 3500, 'led': 'on', 'led_b': None, 'bright': 83,
'buzzer': None, 'child_lock': 'off', 'volume': 50,
'rfid_product_id': '0:0:41:30', 'rfid_tag': '80:52:86:e2:d8:86:4',
'act_sleep': 'close'}
Response of a Air Purifier 2 (zhimi.airpurifier.m1):
{'power': 'on, 'aqi': 10, 'average_aqi': 8, 'humidity': 62,
'temp_dec': 186, 'mode': 'auto', 'favorite_level': 10,
'filter1_life': 80, 'f1_hour_used': 682, 'use_time': 2457000,
'motor1_speed': 354, 'purify_volume': 25262, 'f1_hour': 3500,
'led': 'off', 'led_b': 2, 'bright': None, 'buzzer': 'off',
'child_lock': 'off', 'volume': None}
'motor1_speed': 354, 'motor2_speed': None, 'purify_volume': 25262,
'f1_hour': 3500, 'led': 'off', 'led_b': 2, 'bright': None,
'buzzer': 'off', 'child_lock': 'off', 'volume': None,
'rfid_product_id': None, 'rfid_tag': None,
'act_sleep': 'close'}
A request is limited to 16 properties.
"""
Expand Down Expand Up @@ -152,11 +173,52 @@ def motor_speed(self) -> int:
"""Speed of the motor."""
return self.data["motor1_speed"]

@property
def motor2_speed(self) -> Optional[int]:
"""Speed of the 2nd motor."""
return self.data["motor2_speed"]

@property
def volume(self) -> Optional[int]:
"""Volume of sound notifications [0-100]."""
return self.data["volume"]

@property
def filter_rfid_product_id(self) -> Optional[str]:
"""RFID product ID of installed filter."""
return self.data["rfid_product_id"]

@property
def filter_rfid_tag(self) -> Optional[str]:
"""RFID tag ID of installed filter."""
return self.data["rfid_tag"]

@property
def filter_type(self) -> Optional[FilterType]:
"""Type of installed filter."""
if self.filter_rfid_tag is None:
return None
if self.filter_rfid_tag == '0:0:0:0:0:0:0':
return FilterType.Unknown
if self.filter_rfid_product_id is None:
return FilterType.Regular
return self._get_filter_type(self.filter_rfid_product_id)

@property
def learn_mode(self) -> bool:
"""Return True if Learn Mode is enabled."""
return self.data["act_sleep"] == "single"

@classmethod
def _get_filter_type(cls, product_id: str) -> FilterType:
ft = cls._filter_type_cache.get(product_id, None)
if ft is None:
for filter_re, filter_type in FILTER_TYPE_RE:
if filter_re.match(product_id):
ft = cls._filter_type_cache[product_id] = filter_type
break
return ft

def __repr__(self) -> str:
s = "<AirPurifierStatus power=%s, " \
"aqi=%s, " \
Expand All @@ -175,7 +237,12 @@ def __repr__(self) -> str:
"use_time=%s, " \
"purify_volume=%s, " \
"motor_speed=%s, " \
"volume=%s>" % \
"motor2_speed=%s, " \
"volume=%s, " \
"filter_rfid_product_id=%s, " \
"filter_rfid_tag=%s, " \
"filter_type=%s, " \
"learn_mode=%s>" % \
(self.power,
self.aqi,
self.average_aqi,
Expand All @@ -193,7 +260,12 @@ def __repr__(self) -> str:
self.use_time,
self.purify_volume,
self.motor_speed,
self.volume)
self.motor2_speed,
self.volume,
self.filter_rfid_product_id,
self.filter_rfid_tag,
self.filter_type,
self.learn_mode)
return s


Expand All @@ -205,23 +277,20 @@ def status(self) -> AirPurifierStatus:

properties = ['power', 'aqi', 'average_aqi', 'humidity', 'temp_dec',
'mode', 'favorite_level', 'filter1_life', 'f1_hour_used',
'use_time', 'motor1_speed', 'purify_volume', 'f1_hour',
'use_time', 'motor1_speed', 'motor2_speed',
'purify_volume',
# Second request
'led', 'led_b', 'bright', 'buzzer', 'child_lock',
'volume', ]
'f1_hour', 'led', 'led_b', 'bright', 'buzzer',
'child_lock', 'volume', 'rfid_product_id', 'rfid_tag',
'act_sleep']

# A single request is limited to 16 properties. Therefore the
# properties are divided in two groups here. The second group contains
# some infrequent and independent updated properties.
values = self.send(
"get_prop",
properties[0:13]
)

values.extend(self.send(
"get_prop",
properties[13:]
))
# properties are divided into multiple requests
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:13]))
_props[:] = _props[13:]

properties_count = len(properties)
values_count = len(values)
Expand Down Expand Up @@ -286,3 +355,14 @@ def set_volume(self, volume: int):
raise AirPurifierException("Invalid volume: %s" % volume)

return self.send("set_volume", [volume])

def set_learn_mode(self, learn_mode: bool):
"""Set the Learn Mode on/off."""
if learn_mode:
return self.send("set_act_sleep", ["single"])
else:
return self.send("set_act_sleep", ["close"])

def reset_filter(self):
"""Resets filter hours used and remaining life."""
return self.send('reset_filter1')
71 changes: 70 additions & 1 deletion miio/tests/test_airpurifier.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from unittest import TestCase
from miio import AirPurifier
from miio.airpurifier import OperationMode, LedBrightness, AirPurifierException
from miio.airpurifier import (
OperationMode, LedBrightness, FilterType, AirPurifierException
)
from .dummies import DummyDevice
import pytest

Expand All @@ -19,6 +21,7 @@ def __init__(self, *args, **kwargs):
'f1_hour_used': 682,
'use_time': 2457000,
'motor1_speed': 354,
'motor2_speed': 800,
'purify_volume': 25262,
'f1_hour': 3500,
'led': 'off',
Expand All @@ -27,6 +30,9 @@ def __init__(self, *args, **kwargs):
'buzzer': 'off',
'child_lock': 'off',
'volume': 50,
'rfid_product_id': '0:0:41:30',
'rfid_tag': '10:20:30:40:50:60:7',
'act_sleep': 'close',
}
self.return_values = {
'get_prop': self._get_state,
Expand All @@ -39,6 +45,11 @@ def __init__(self, *args, **kwargs):
lambda x: self._set_state("favorite_level", x),
'set_led_b': lambda x: self._set_state("led_b", x),
'set_volume': lambda x: self._set_state("volume", x),
'set_act_sleep': lambda x: self._set_state("act_sleep", x),
'reset_filter1': lambda x: (
self._set_state('f1_hour_used', [0]),
self._set_state('filter1_life', [100])
)
}
super().__init__(args, kwargs)

Expand Down Expand Up @@ -87,6 +98,7 @@ def test_status(self):
assert self.state().filter_hours_used == self.device.start_state["f1_hour_used"]
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().motor_speed == self.device.start_state["motor1_speed"]
assert self.state().motor2_speed == self.device.start_state["motor2_speed"]
assert self.state().purify_volume == self.device.start_state["purify_volume"]

assert self.state().led == (self.device.start_state["led"] == 'on')
Expand All @@ -95,6 +107,8 @@ def test_status(self):
assert self.state().child_lock == (self.device.start_state["child_lock"] == 'on')
assert self.state().illuminance == self.device.start_state["bright"]
assert self.state().volume == self.device.start_state["volume"]
assert self.state().filter_rfid_product_id == self.device.start_state["rfid_product_id"]
assert self.state().filter_rfid_tag == self.device.start_state["rfid_tag"]

def test_set_mode(self):
def mode():
Expand Down Expand Up @@ -189,6 +203,30 @@ def volume():
with pytest.raises(AirPurifierException):
self.device.set_volume(101)

def test_set_learn_mode(self):
def learn_mode():
return self.device.status().learn_mode

self.device.set_learn_mode(True)
assert learn_mode() is True

self.device.set_learn_mode(False)
assert learn_mode() is False

def test_reset_filter(self):
def filter_hours_used():
return self.device.status().filter_hours_used

def filter_life_remaining():
return self.device.status().filter_life_remaining

self.device._reset_state()
assert filter_hours_used() != 0
assert filter_life_remaining() != 100
self.device.reset_filter()
assert filter_hours_used() == 0
assert filter_life_remaining() == 100

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

Expand Down Expand Up @@ -219,3 +257,34 @@ def test_status_without_buzzer(self):
# The Air Purifier Pro doesn't provide the buzzer property
self.device.state["buzzer"] = None
assert self.state().buzzer is None

def test_status_without_motor2_speed(self):
self.device._reset_state()
# The Air Purifier Pro doesn't provide the buzzer property
self.device.state["motor2_speed"] = None
assert self.state().motor2_speed is None

def test_status_without_filter_rfid_tag(self):
self.device._reset_state()
self.device.state["rfid_tag"] = None
assert self.state().filter_rfid_tag is None
assert self.state().filter_type is None

def test_status_with_filter_rfid_tag_zeros(self):
self.device._reset_state()
self.device.state["rfid_tag"] = '0:0:0:0:0:0:0'
assert self.state().filter_type is FilterType.Unknown

def test_status_without_filter_rfid_product_id(self):
self.device._reset_state()
self.device.state["rfid_product_id"] = None
assert self.state().filter_type is FilterType.Regular

def test_status_filter_rfid_product_ids(self):
self.device._reset_state()
self.device.state["rfid_product_id"] = '0:0:30:31'
assert self.state().filter_type is FilterType.AntiFormaldehyde
self.device.state["rfid_product_id"] = '0:0:30:32'
assert self.state().filter_type is FilterType.Regular
self.device.state["rfid_product_id"] = '0:0:41:30'
assert self.state().filter_type is FilterType.AntiBacterial

0 comments on commit 18d422f

Please sign in to comment.