import enum
import logging
from collections import defaultdict
from typing import Any, Dict, Optional
import click
from miio import Device, DeviceStatus
from miio.click_common import EnumType, command, format_output
from .airfilter_util import FilterType, FilterTypeUtil
_LOGGER = logging.getLogger(__name__)
SUPPORTED_MODELS = [
"zhimi.airpurifier.v1",
"zhimi.airpurifier.v2",
"zhimi.airpurifier.v3",
"zhimi.airpurifier.v5",
"zhimi.airpurifier.v6",
"zhimi.airpurifier.v7",
"zhimi.airpurifier.m1",
"zhimi.airpurifier.m2",
"zhimi.airpurifier.ma1",
"zhimi.airpurifier.ma2",
"zhimi.airpurifier.sa1",
"zhimi.airpurifier.sa2",
"zhimi.airpurifier.mc1",
"zhimi.airpurifier.mc2",
]
[docs]
class OperationMode(enum.Enum):
# Supported modes of the Air Purifier Pro, 2, V3
Auto = "auto"
Silent = "silent"
Favorite = "favorite"
# Additional supported modes of the Air Purifier 2 and V3
Idle = "idle"
# Additional supported modes of the Air Purifier V3
Medium = "medium"
High = "high"
Strong = "strong"
# Additional supported modes of the Air Purifier Super 2
Low = "low"
[docs]
class SleepMode(enum.Enum):
Off = "poweroff"
Silent = "silent"
Idle = "idle"
[docs]
class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
[docs]
class AirPurifierStatus(DeviceStatus):
"""Container for status reports from the air purifier."""
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, '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 Pro (zhimi.airpurifier.v7):
{'power': 'on', 'aqi': 2, 'average_aqi': 3, 'humidity': 42,
'temp_dec': 223, 'mode': 'favorite', 'favorite_level': 3,
'filter1_life': 56, 'f1_hour_used': 1538, 'use_time': None,
'motor1_speed': 300, 'motor2_speed': 898, 'purify_volume': None,
'f1_hour': 3500, 'led': 'on', 'led_b': None, 'bright': 45,
'buzzer': None, 'child_lock': 'off', 'volume': 0,
'rfid_product_id': '0:0:30:33', 'rfid_tag': '80:6a:a9:e2:37:92:4',
'act_sleep': None, 'sleep_mode': None, 'sleep_time': None,
'sleep_data_num': None, 'app_extra': 0, 'act_det': None,
'button_pressed': None}
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, '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'}
Response of a Air Purifier 2 (zhimi.airpurifier.m2):
{'power': 'on', 'aqi': 10, 'average_aqi': 8, 'humidity': 42,
'temp_dec': 223, 'mode': 'favorite', 'favorite_level': 2,
'filter1_life': 63, 'f1_hour_used': 1282, 'use_time': 16361416,
'motor1_speed': 747, 'motor2_speed': None, 'purify_volume': 421580,
'f1_hour': 3500, 'led': 'on', 'led_b': 1, 'bright': None,
'buzzer': 'off', 'child_lock': 'off', 'volume': None,
'rfid_product_id': None, 'rfid_tag': None, 'act_sleep': 'close',
'sleep_mode': 'idle', 'sleep_time': 86168, 'sleep_data_num': 30,
'app_extra': 0, 'act_det': None, 'button_pressed': None}
Response of a Air Purifier V3 (zhimi.airpurifier.v3)
{'power': 'off', 'aqi': 0, 'humidity': None, 'temp_dec': None,
'mode': 'idle', 'led': 'off', 'led_b': 10, 'buzzer': 'on',
'child_lock': 'off', 'bright': 43, 'favorite_level': None,
'filter1_life': 26, 'f1_hour_used': 2573, 'use_time': None,
'motor1_speed': 0}
{'power': 'on', 'aqi': 18, 'humidity': None, 'temp_dec': None,
'mode': 'silent', 'led': 'off', 'led_b': 10, 'buzzer': 'on',
'child_lock': 'off', 'bright': 4, 'favorite_level': None,
'filter1_life': 26, 'f1_hour_used': 2574, 'use_time': None,
'motor1_speed': 648}
A request is limited to 16 properties.
"""
self.filter_type_util = FilterTypeUtil()
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.power == "on"
@property
def aqi(self) -> int:
"""Air quality index."""
return self.data["aqi"]
@property
def average_aqi(self) -> int:
"""Average of the air quality index."""
return self.data["average_aqi"]
@property
def humidity(self) -> int:
"""Current humidity."""
return self.data["humidity"]
@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if self.data["temp_dec"] is not None:
return self.data["temp_dec"] / 10.0
return None
@property
def mode(self) -> OperationMode:
"""Current operation mode."""
return OperationMode(self.data["mode"])
@property
def sleep_mode(self) -> Optional[SleepMode]:
"""Operation mode of the sleep state.
(Idle vs. Silent)
"""
if self.data["sleep_mode"] is not None:
return SleepMode(self.data["sleep_mode"])
return None
@property
def led(self) -> bool:
"""Return True if LED is on."""
return self.data["led"] == "on"
@property
def led_brightness(self) -> Optional[LedBrightness]:
"""Brightness of the LED."""
if self.data["led_b"] is not None:
try:
return LedBrightness(self.data["led_b"])
except ValueError:
return None
return None
@property
def illuminance(self) -> Optional[int]:
"""Environment illuminance level in lux [0-200].
Sensor value is updated only when device is turned on.
"""
return self.data["bright"]
@property
def buzzer(self) -> Optional[bool]:
"""Return True if buzzer is on."""
if self.data["buzzer"] is not None:
return self.data["buzzer"] == "on"
return None
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"] == "on"
@property
def favorite_level(self) -> int:
"""Return favorite level, which is used if the mode is ``favorite``."""
# Favorite level used when the mode is `favorite`.
return self.data["favorite_level"]
@property
def filter_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["filter1_life"]
@property
def filter_hours_used(self) -> int:
"""How long the filter has been in use."""
return self.data["f1_hour_used"]
@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
def purify_volume(self) -> int:
"""The volume of purified air in cubic meter."""
return self.data["purify_volume"]
@property
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."""
return self.filter_type_util.determine_filter_type(
self.filter_rfid_tag, self.filter_rfid_product_id
)
@property
def learn_mode(self) -> bool:
"""Return True if Learn Mode is enabled."""
return self.data["act_sleep"] == "single"
@property
def sleep_time(self) -> Optional[int]:
return self.data["sleep_time"]
@property
def sleep_mode_learn_count(self) -> Optional[int]:
return self.data["sleep_data_num"]
@property
def extra_features(self) -> Optional[int]:
return self.data["app_extra"]
@property
def turbo_mode_supported(self) -> Optional[bool]:
if self.data["app_extra"] is not None:
return self.data["app_extra"] == 1
return None
@property
def auto_detect(self) -> Optional[bool]:
"""Return True if auto detect is enabled."""
if self.data["act_det"] is not None:
return self.data["act_det"] == "on"
return None
@property
def button_pressed(self) -> Optional[str]:
"""Last pressed button."""
return self.data["button_pressed"]
[docs]
class AirPurifier(Device):
"""Main class representing the air purifier."""
_supported_models = SUPPORTED_MODELS
[docs]
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"AQI: {result.aqi} μg/m³\n"
"Average AQI: {result.average_aqi} μg/m³\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"Mode: {result.mode.value}\n"
"LED: {result.led}\n"
"LED brightness: {result.led_brightness}\n"
"Illuminance: {result.illuminance} lx\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Favorite level: {result.favorite_level}\n"
"Filter life remaining: {result.filter_life_remaining} %\n"
"Filter hours used: {result.filter_hours_used}\n"
"Use time: {result.use_time} s\n"
"Purify volume: {result.purify_volume} m³\n"
"Motor speed: {result.motor_speed} rpm\n"
"Motor 2 speed: {result.motor2_speed} rpm\n"
"Sound volume: {result.volume} %\n"
"Filter RFID product id: {result.filter_rfid_product_id}\n"
"Filter RFID tag: {result.filter_rfid_tag}\n"
"Filter type: {result.filter_type}\n"
"Learn mode: {result.learn_mode}\n"
"Sleep mode: {result.sleep_mode}\n"
"Sleep time: {result.sleep_time}\n"
"Sleep mode learn count: {result.sleep_mode_learn_count}\n"
"AQI sensor enabled on power off: {result.auto_detect}\n",
)
)
def status(self) -> AirPurifierStatus:
"""Retrieve properties."""
properties = [
"power",
"aqi",
"average_aqi",
"humidity",
"temp_dec",
"mode",
"favorite_level",
"filter1_life",
"f1_hour_used",
"use_time",
"motor1_speed",
"motor2_speed",
"purify_volume",
"f1_hour",
"led",
# Second request
"led_b",
"bright",
"buzzer",
"child_lock",
"volume",
"rfid_product_id",
"rfid_tag",
"act_sleep",
"sleep_mode",
"sleep_time",
"sleep_data_num",
"app_extra",
"act_det",
"button_pressed",
]
values = self.get_properties(properties, max_properties=15)
return AirPurifierStatus(defaultdict(lambda: None, zip(properties, values)))
[docs]
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
[docs]
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
[docs]
@command(
click.argument("mode", type=EnumType(OperationMode)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.send("set_mode", [mode.value])
[docs]
@command(
click.argument("level", type=int),
default_output=format_output("Setting favorite level to {level}"),
)
def set_favorite_level(self, level: int):
"""Set favorite level."""
if level < 0 or level > 17:
raise ValueError("Invalid favorite level: %s" % level)
# Possible alternative property: set_speed_favorite
# Set the favorite level used when the mode is `favorite`,
# should be between 0 and 17.
return self.send("set_level_favorite", [level]) # 0 ... 17
[docs]
@command(
click.argument("brightness", type=EnumType(LedBrightness)),
default_output=format_output("Setting LED brightness to {brightness}"),
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
return self.send("set_led_b", [brightness.value])
[docs]
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
if led:
return self.send("set_led", ["on"])
else:
return self.send("set_led", ["off"])
[docs]
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if buzzer:
return self.send("set_buzzer", ["on"])
else:
return self.send("set_buzzer", ["off"])
[docs]
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])
[docs]
@command(
click.argument("volume", type=int),
default_output=format_output("Setting sound volume to {volume}"),
)
def set_volume(self, volume: int):
"""Set volume of sound notifications [0-100]."""
if volume < 0 or volume > 100:
raise ValueError("Invalid volume: %s" % volume)
return self.send("set_volume", [volume])
[docs]
@command(
click.argument("learn_mode", type=bool),
default_output=format_output(
lambda learn_mode: (
"Turning on learn mode" if learn_mode else "Turning off learn mode"
)
),
)
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"])
[docs]
@command(
click.argument("auto_detect", type=bool),
default_output=format_output(
lambda auto_detect: (
"Turning on auto detect" if auto_detect else "Turning off auto detect"
)
),
)
def set_auto_detect(self, auto_detect: bool):
"""Set auto detect on/off.
It's a feature of the AirPurifier V1 & V3
"""
if auto_detect:
return self.send("set_act_det", ["on"])
else:
return self.send("set_act_det", ["off"])
[docs]
@command(default_output=format_output("Resetting filter"))
def reset_filter(self):
"""Resets filter hours used and remaining life."""
return self.send("reset_filter1")