Skip to content

Commit

Permalink
Air purifier 3/3H support (remastered) (#634)
Browse files Browse the repository at this point in the history
* Add basic support for Xiaomi Mi Air Purifier 3/3H. In order to support that, also implement MiotDevice class with basic support for MIoT protocol.

* Extract functionality for determining Xiaomi air filter type into a separate util class

* Rename airfilter.py to airfilter_util.py to indicate it's not referring to an actual device

* Tests for miot purifier

# Conflicts:
#	miio/ceil_cli.py
#	miio/device.py
#	miio/philips_eyecare_cli.py
#	miio/plug_cli.py
#	miio/tests/dummies.py
#	miio/tests/test_airconditioningcompanion.py
#	miio/tests/test_wifirepeater.py
#	miio/vacuum.py
#	miio/vacuum_cli.py

* MIoT Air Purifier: add support for fine-tuning favourite mode by "set_favorite_rpm"

* MIoT Air Purifier: improve comments

* airpurifier_miot: don't try to retrieve "button_pressed" as it errors out if no button was pressed since purifier started up

* Version

* Fix MIOT purifier tests

* Added buzzer_volumw and handling missing features for miot

* Fixed volume setter to be same as in non-miot purifier

* Fixed incorrect comment body

* Review comments fixed

* Expanded documentation

Co-authored-by: Petr Kotek <kotekp@google.com>
  • Loading branch information
foxel and Petr Kotek authored Mar 15, 2020
1 parent f0d4565 commit 5f8aa72
Show file tree
Hide file tree
Showing 10 changed files with 798 additions and 33 deletions.
16 changes: 16 additions & 0 deletions docs/miio.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ miio\.airpurifier module
:show-inheritance:
:undoc-members:

miio\.airpurifier_miot module
-----------------------------

.. automodule:: miio.airpurifier_miot
:members:
:show-inheritance:
:undoc-members:

miio\.airqualitymonitor module
------------------------------

Expand Down Expand Up @@ -93,6 +101,14 @@ miio\.device module
:show-inheritance:
:undoc-members:

miio\.miot_device module
------------------------

.. automodule:: miio.miot_device
:members:
:show-inheritance:
:undoc-members:

miio\.discovery module
----------------------

Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1
from miio.airhumidifier_mjjsq import AirHumidifierMjjsq
from miio.airpurifier import AirPurifier
from miio.airpurifier_miot import AirPurifierMiot
from miio.airqualitymonitor import AirQualityMonitor
from miio.aqaracamera import AqaraCamera
from miio.ceil import Ceil
Expand Down
47 changes: 47 additions & 0 deletions miio/airfilter_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import enum
import re
from typing import Optional


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 FilterTypeUtil:
"""Utility class for determining xiaomi air filter type."""

_filter_type_cache = {}

def determine_filter_type(
self, rfid_tag: Optional[str], product_id: Optional[str]
) -> Optional[FilterType]:
"""
Determine Xiaomi air filter type based on its product ID.
:param rfid_tag: RFID tag value
:param product_id: Product ID such as "0:0:30:33"
"""
if rfid_tag is None:
return None
if rfid_tag == "0:0:0:0:0:0:0":
return FilterType.Unknown
if product_id is None:
return FilterType.Regular

ft = self._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 = self._filter_type_cache[product_id] = filter_type
break
return ft
39 changes: 6 additions & 33 deletions miio/airpurifier.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import enum
import logging
import re
from collections import defaultdict
from typing import Any, Dict, Optional

import click

from .airfilter_util import FilterType, FilterTypeUtil
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
Expand Down Expand Up @@ -42,20 +42,6 @@ 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."""

Expand Down Expand Up @@ -102,6 +88,7 @@ def __init__(self, data: Dict[str, Any]) -> None:
A request is limited to 16 properties.
"""

self.filter_type_util = FilterTypeUtil()
self.data = data

@property
Expand Down Expand Up @@ -239,13 +226,9 @@ def filter_rfid_tag(self) -> Optional[str]:
@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)
return self.filter_type_util.determine_filter_type(
self.filter_rfid_tag, self.filter_rfid_product_id
)

@property
def learn_mode(self) -> bool:
Expand Down Expand Up @@ -284,16 +267,6 @@ def button_pressed(self) -> Optional[str]:
"""Last pressed button."""
return self.data["button_pressed"]

@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, "
Expand Down Expand Up @@ -538,7 +511,7 @@ def set_child_lock(self, lock: bool):

@command(
click.argument("volume", type=int),
default_output=format_output("Setting favorite level to {volume}"),
default_output=format_output("Setting sound volume to {volume}"),
)
def set_volume(self, volume: int):
"""Set volume of sound notifications [0-100]."""
Expand Down
Loading

0 comments on commit 5f8aa72

Please sign in to comment.