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
_LOGGER = logging.getLogger(__name__)
MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2"
MODEL_AIRFRESH_VA4 = "zhimi.airfresh.va4"
AVAILABLE_PROPERTIES_COMMON = [
"power",
"temp_dec",
"aqi",
"average_aqi",
"co2",
"buzzer",
"child_lock",
"humidity",
"led_level",
"mode",
"motor1_speed",
"use_time",
"ntcT",
"app_extra",
"f1_hour_used",
"filter_life",
"f_hour",
"favorite_level",
"led",
]
AVAILABLE_PROPERTIES = {
MODEL_AIRFRESH_VA2: AVAILABLE_PROPERTIES_COMMON,
MODEL_AIRFRESH_VA4: AVAILABLE_PROPERTIES_COMMON + ["ptc_state"],
}
[docs]
class OperationMode(enum.Enum):
# Supported modes of the Air Fresh VA2 (zhimi.airfresh.va2)
Auto = "auto"
Silent = "silent"
Interval = "interval"
Low = "low"
Middle = "middle"
Strong = "strong"
[docs]
class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
[docs]
class AirFreshStatus(DeviceStatus):
"""Container for status reports from the air fresh."""
def __init__(self, data: Dict[str, Any], model: str) -> None:
"""
Response of a Air Fresh VA4 (zhimi.airfresh.va4):
{
'power': 'on',
'temp_dec': 28.5,
'aqi': 1,
'average_aqi': 1,
'co2': 1081,
'buzzer': 'off',
'child_lock': 'off',
'humidity': 40,
'led_level': 1,
'mode': 'silent',
'motor1_speed': 400,
'use_time': 510000,
'ntcT': 33.53,
'app_extra': None,
'f1_hour_used': 141,
'filter_life': None,
'f_hour': None,
'favorite_level': None,
'led': None,
'ptc_state': 'off',
}
"""
self.data = data
self.model = model
@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 co2(self) -> int:
"""Carbon dioxide."""
return self.data["co2"]
@property
def humidity(self) -> int:
"""Current humidity."""
return self.data["humidity"]
@property
def ptc(self) -> Optional[bool]:
"""Return True if PTC is on."""
if self.data["ptc_state"] is not None:
return self.data["ptc_state"] == "on"
return None
@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if self.data["temp_dec"] is not None:
if self.model == MODEL_AIRFRESH_VA4:
return self.data["temp_dec"]
else:
return self.data["temp_dec"] / 10.0
return None
@property
def ntc_temperature(self) -> Optional[float]:
"""Current ntc temperature, if available."""
if self.data["ntcT"] is not None:
return self.data["ntcT"]
return None
@property
def mode(self) -> OperationMode:
"""Current operation mode."""
return OperationMode(self.data["mode"])
@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_level"] is not None:
try:
return LedBrightness(self.data["led_level"])
except ValueError:
_LOGGER.error(
"Unsupported LED brightness discarded: %s", self.data["led_level"]
)
return None
return None
@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 filter_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["filter_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 motor_speed(self) -> int:
"""Speed of the motor."""
return self.data["motor1_speed"]
@property
def extra_features(self) -> Optional[int]:
return self.data["app_extra"]
[docs]
class AirFresh(Device):
"""Main class representing the air fresh."""
_supported_models = list(AVAILABLE_PROPERTIES.keys())
[docs]
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Heater (PTC): {result.ptc}\n"
"AQI: {result.aqi} μg/m³\n"
"Average AQI: {result.average_aqi} μg/m³\n"
"Temperature: {result.temperature} °C\n"
"NTC temperature: {result.ntc_temperature} °C\n"
"Humidity: {result.humidity} %\n"
"CO2: {result.co2} %\n"
"Mode: {result.mode.value}\n"
"LED: {result.led}\n"
"LED brightness: {result.led_brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Filter life remaining: {result.filter_life_remaining} %\n"
"Filter hours used: {result.filter_hours_used}\n"
"Use time: {result.use_time} s\n"
"Motor speed: {result.motor_speed} rpm\n",
)
)
def status(self) -> AirFreshStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES.get(
self.model, AVAILABLE_PROPERTIES[MODEL_AIRFRESH_VA2]
)
values = self.get_properties(properties, max_properties=15)
return AirFreshStatus(
defaultdict(lambda: None, zip(properties, values)), self.model
)
[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("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("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_level", [brightness.value])
[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(default_output=format_output("Resetting filter"))
def reset_filter(self):
"""Resets filter hours used and remaining life."""
return self.send("reset_filter1")
[docs]
@command(
click.argument("ptc", type=bool),
default_output=format_output(
lambda buzzer: "Turning on PTC" if buzzer else "Turning off PTC"
),
)
def set_ptc(self, ptc: bool):
"""Set PTC on/off."""
if ptc:
return self.send("set_ptc_state", ["on"])
else:
return self.send("set_ptc_state", ["off"])