import enum
import logging
from typing import Any, Dict, Optional
import click
from miio import DeviceStatus, MiotDevice
from miio.click_common import EnumType, command, format_output
_LOGGER = logging.getLogger(__name__)
SMARTMI_EVAPORATIVE_HUMIDIFIER_2 = "zhimi.humidifier.ca4"
_MAPPINGS = {
SMARTMI_EVAPORATIVE_HUMIDIFIER_2: {
# Source http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:humidifier:0000A00E:zhimi-ca4:2
# Air Humidifier (siid=2)
"power": {"siid": 2, "piid": 1}, # bool
"fault": {"siid": 2, "piid": 2}, # [0, 15] step 1
"mode": {"siid": 2, "piid": 5}, # 0 - Auto, 1 - lvl1, 2 - lvl2, 3 - lvl3
"target_humidity": {"siid": 2, "piid": 6}, # [30, 80] step 1
"water_level": {"siid": 2, "piid": 7}, # [0, 128] step 1
"dry": {"siid": 2, "piid": 8}, # bool
"use_time": {"siid": 2, "piid": 9}, # [0, 2147483600], step 1
"button_pressed": {"siid": 2, "piid": 10}, # 0 - none, 1 - led, 2 - power
"speed_level": {"siid": 2, "piid": 11}, # [200, 2000], step 10
# Environment (siid=3)
"temperature": {"siid": 3, "piid": 7}, # [-40, 125] step 0.1
"fahrenheit": {"siid": 3, "piid": 8}, # [-40, 257] step 0.1
"humidity": {"siid": 3, "piid": 9}, # [0, 100] step 1
# Alarm (siid=4)
"buzzer": {"siid": 4, "piid": 1},
# Indicator Light (siid=5)
"led_brightness": {"siid": 5, "piid": 2}, # 0 - Off, 1 - Dim, 2 - Brightest
# Physical Control Locked (siid=6)
"child_lock": {"siid": 6, "piid": 1}, # bool
# Other (siid=7)
"actual_speed": {"siid": 7, "piid": 1}, # [0, 2000] step 1
"power_time": {"siid": 7, "piid": 3}, # [0, 4294967295] step 1
"clean_mode": {"siid": 7, "piid": 5}, # bool
}
}
[docs]
class OperationMode(enum.Enum):
Auto = 0
Low = 1
Mid = 2
High = 3
[docs]
class LedBrightness(enum.Enum):
Off = 0
Dim = 1
Bright = 2
[docs]
class AirHumidifierMiotStatus(DeviceStatus):
"""Container for status reports from the air humidifier.
Xiaomi Smartmi Evaporation Air Humidifier 2 (zhimi.humidifier.ca4) respone (MIoT format)::
[
{'did': 'power', 'siid': 2, 'piid': 1, 'code': 0, 'value': True},
{'did': 'fault', 'siid': 2, 'piid': 2, 'code': 0, 'value': 0},
{'did': 'mode', 'siid': 2, 'piid': 5, 'code': 0, 'value': 0},
{'did': 'target_humidity', 'siid': 2, 'piid': 6, 'code': 0, 'value': 50},
{'did': 'water_level', 'siid': 2, 'piid': 7, 'code': 0, 'value': 127},
{'did': 'dry', 'siid': 2, 'piid': 8, 'code': 0, 'value': False},
{'did': 'use_time', 'siid': 2, 'piid': 9, 'code': 0, 'value': 5140816},
{'did': 'button_pressed', 'siid': 2, 'piid': 10, 'code': 0, 'value': 2},
{'did': 'speed_level', 'siid': 2, 'piid': 11, 'code': 0, 'value': 790},
{'did': 'temperature', 'siid': 3, 'piid': 7, 'code': 0, 'value': 22.7},
{'did': 'fahrenheit', 'siid': 3, 'piid': 8, 'code': 0, 'value': 72.8},
{'did': 'humidity', 'siid': 3, 'piid': 9, 'code': 0, 'value': 39},
{'did': 'buzzer', 'siid': 4, 'piid': 1, 'code': 0, 'value': False},
{'did': 'led_brightness', 'siid': 5, 'piid': 2, 'code': 0, 'value': 2},
{'did': 'child_lock', 'siid': 6, 'piid': 1, 'code': 0, 'value': False},
{'did': 'actual_speed', 'siid': 7, 'piid': 1, 'code': 0, 'value': 0},
{'did': 'power_time', 'siid': 7, 'piid': 3, 'code': 0, 'value': 18520},
{'did': 'clean_mode', 'siid': 7, 'piid': 5, 'code': 0, 'value': True}
]
"""
def __init__(self, data: Dict[str, Any]) -> None:
self.data = data
# Air Humidifier
@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.data["power"]
@property
def power(self) -> str:
"""Return power state."""
return "on" if self.is_on else "off"
@property
def error(self) -> int:
"""Return error state."""
return self.data["fault"]
@property
def mode(self) -> OperationMode:
"""Return current operation mode."""
try:
mode = OperationMode(self.data["mode"])
except ValueError as e:
_LOGGER.exception("Cannot parse mode: %s", e)
return OperationMode.Auto
return mode
@property
def target_humidity(self) -> int:
"""Return target humidity."""
return self.data["target_humidity"]
@property
def water_level(self) -> Optional[int]:
"""Return current water level in percent.
If water tank is full, raw water_level value is 120. If water tank is
overfilled, raw water_level value is 125.
"""
water_level = self.data["water_level"]
if water_level > 125:
return None
if water_level < 0:
return 0
return int(min(water_level / 1.2, 100))
@property
def water_tank_detached(self) -> bool:
"""True if the water tank is detached.
If water tank is detached, water_level is 127.
"""
return self.data["water_level"] == 127
@property
def dry(self) -> Optional[bool]:
"""Return True if dry mode is on."""
if self.data["dry"] is not None:
return self.data["dry"]
return None
@property
def use_time(self) -> int:
"""Return how long the device has been active in seconds."""
return self.data["use_time"]
@property
def button_pressed(self) -> PressedButton:
"""Return last pressed button."""
try:
button = PressedButton(self.data["button_pressed"])
except ValueError as e:
_LOGGER.exception("Cannot parse button_pressed: %s", e)
return PressedButton.No
return button
@property
def motor_speed(self) -> int:
"""Return target speed of the motor."""
return self.data["speed_level"]
# Environment
@property
def humidity(self) -> int:
"""Return current humidity."""
return self.data["humidity"]
@property
def temperature(self) -> Optional[float]:
"""Return current temperature, if available."""
if self.data["temperature"] is not None:
return round(self.data["temperature"], 1)
return None
@property
def fahrenheit(self) -> Optional[float]:
"""Return current temperature in fahrenheit, if available."""
if self.data["fahrenheit"] is not None:
return round(self.data["fahrenheit"], 1)
return None
# Alarm
@property
def buzzer(self) -> Optional[bool]:
"""Return True if buzzer is on."""
if self.data["buzzer"] is not None:
return self.data["buzzer"]
return None
# Indicator Light
@property
def led_brightness(self) -> Optional[LedBrightness]:
"""Return brightness of the LED."""
if self.data["led_brightness"] is not None:
try:
return LedBrightness(self.data["led_brightness"])
except ValueError as e:
_LOGGER.exception("Cannot parse led_brightness: %s", e)
return None
return None
# Physical Control Locked
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"]
# Other
@property
def actual_speed(self) -> int:
"""Return real speed of the motor."""
return self.data["actual_speed"]
@property
def power_time(self) -> int:
"""Return how long the device has been powered in seconds."""
return self.data["power_time"]
@property
def clean_mode(self) -> bool:
"""Return True if clean mode is active."""
return self.data["clean_mode"]
[docs]
class AirHumidifierMiot(MiotDevice):
"""Main class representing the air humidifier which uses MIoT protocol."""
_mappings = _MAPPINGS
[docs]
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Error: {result.error}\n"
"Target Humidity: {result.target_humidity} %\n"
"Humidity: {result.humidity} %\n"
"Temperature: {result.temperature} °C\n"
"Temperature: {result.fahrenheit} °F\n"
"Water Level: {result.water_level} %\n"
"Water tank detached: {result.water_tank_detached}\n"
"Mode: {result.mode}\n"
"LED brightness: {result.led_brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Dry mode: {result.dry}\n"
"Button pressed {result.button_pressed}\n"
"Target motor speed: {result.motor_speed} rpm\n"
"Actual motor speed: {result.actual_speed} rpm\n"
"Use time: {result.use_time} s\n"
"Power time: {result.power_time} s\n"
"Clean mode: {result.clean_mode}\n",
)
)
def status(self) -> AirHumidifierMiotStatus:
"""Retrieve properties."""
return AirHumidifierMiotStatus(
{
prop["did"]: prop["value"] if prop["code"] == 0 else None
for prop in self.get_properties_for_mapping()
}
)
[docs]
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.set_property("power", True)
[docs]
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.set_property("power", False)
[docs]
@command(
click.argument("rpm", type=int),
default_output=format_output("Setting motor speed '{rpm}' rpm"),
)
def set_speed(self, rpm: int):
"""Set motor speed."""
if rpm < 200 or rpm > 2000 or rpm % 10 != 0:
raise ValueError(
"Invalid motor speed: %s. Must be between 200 and 2000 and divisible by 10"
% rpm
)
return self.set_property("speed_level", rpm)
[docs]
@command(
click.argument("humidity", type=int),
default_output=format_output("Setting target humidity {humidity}%"),
)
def set_target_humidity(self, humidity: int):
"""Set target humidity."""
if humidity < 30 or humidity > 80:
raise ValueError(
"Invalid target humidity: %s. Must be between 30 and 80" % humidity
)
return self.set_property("target_humidity", humidity)
[docs]
@command(
click.argument("mode", type=EnumType(OperationMode)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set working mode."""
return self.set_property("mode", mode.value)
[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.set_property("led_brightness", 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."""
return self.set_property("buzzer", buzzer)
[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."""
return self.set_property("child_lock", lock)
[docs]
@command(
click.argument("dry", type=bool),
default_output=format_output(
lambda dry: "Turning on dry mode" if dry else "Turning off dry mode"
),
)
def set_dry(self, dry: bool):
"""Set dry mode on/off."""
return self.set_property("dry", dry)
[docs]
@command(
click.argument("clean_mode", type=bool),
default_output=format_output(
lambda clean_mode: (
"Turning on clean mode" if clean_mode else "Turning off clean mode"
)
),
)
def set_clean_mode(self, clean_mode: bool):
"""Set clean mode on/off."""
return self.set_property("clean_mode", clean_mode)