Source code for miio.integrations.zhimi.humidifier.airhumidifier

import enum
import logging
from collections import defaultdict
from typing import Any, Dict, Optional

import click

from miio import Device, DeviceError, DeviceInfo, DeviceStatus
from miio.click_common import EnumType, command, format_output
from miio.devicestatus import sensor, setting

_LOGGER = logging.getLogger(__name__)

MODEL_HUMIDIFIER_V1 = "zhimi.humidifier.v1"
MODEL_HUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
MODEL_HUMIDIFIER_CB1 = "zhimi.humidifier.cb1"
MODEL_HUMIDIFIER_CB2 = "zhimi.humidifier.cb2"

SUPPORTED_MODELS = [
    MODEL_HUMIDIFIER_V1,
    MODEL_HUMIDIFIER_CA1,
    MODEL_HUMIDIFIER_CB1,
    MODEL_HUMIDIFIER_CB2,
]

AVAILABLE_PROPERTIES_COMMON = [
    "power",
    "mode",
    "humidity",
    "buzzer",
    "led_b",
    "child_lock",
    "limit_hum",
    "use_time",
    "hw_version",
]

AVAILABLE_PROPERTIES = {
    MODEL_HUMIDIFIER_V1: AVAILABLE_PROPERTIES_COMMON
    + ["temp_dec", "trans_level", "button_pressed"],
    MODEL_HUMIDIFIER_CA1: AVAILABLE_PROPERTIES_COMMON
    + ["temp_dec", "speed", "depth", "dry"],
    MODEL_HUMIDIFIER_CB1: AVAILABLE_PROPERTIES_COMMON
    + ["temperature", "speed", "depth", "dry"],
    MODEL_HUMIDIFIER_CB2: AVAILABLE_PROPERTIES_COMMON
    + ["temperature", "speed", "depth", "dry"],
}


[docs] class OperationMode(enum.Enum): Silent = "silent" Medium = "medium" High = "high" Auto = "auto" Strong = "strong"
[docs] class LedBrightness(enum.Enum): Bright = 0 Dim = 1 Off = 2
[docs] class AirHumidifierStatus(DeviceStatus): """Container for status reports from the air humidifier.""" def __init__(self, data: Dict[str, Any], device_info: DeviceInfo) -> None: """Response of a Air Humidifier (zhimi.humidifier.v1): {'power': 'off', 'mode': 'high', 'temp_dec': 294, 'humidity': 33, 'buzzer': 'on', 'led_b': 0, 'child_lock': 'on', 'limit_hum': 40, 'trans_level': 85, 'speed': None, 'depth': None, 'dry': None, 'use_time': 941100, 'hw_version': 0, 'button_pressed': 'led'} """ self.data = data self.device_info = device_info @property def power(self) -> str: """Power state.""" return self.data["power"] @property def is_on(self) -> bool: """True if device is turned on.""" return self.power == "on" @property def mode(self) -> OperationMode: """Operation mode. Can be either silent, medium or high. """ return OperationMode(self.data["mode"]) @property @sensor("Temperature", unit="°C", device_class="temperature") def temperature(self) -> Optional[float]: """Current temperature, if available.""" if "temp_dec" in self.data and self.data["temp_dec"] is not None: return self.data["temp_dec"] / 10.0 if "temperature" in self.data and self.data["temperature"] is not None: return self.data["temperature"] return None @property @sensor("Humidity", unit="%", device_class="humidity") def humidity(self) -> int: """Current humidity.""" return self.data["humidity"] @property @setting( name="Buzzer", icon="mdi:volume-high", setter_name="set_buzzer", device_class="switch", entity_category="config", ) def buzzer(self) -> bool: """True if buzzer is turned on.""" return self.data["buzzer"] == "on" @property @setting( name="Led Brightness", icon="mdi:brightness-6", setter_name="set_led_brightness", choices=LedBrightness, entity_category="config", ) def led_brightness(self) -> Optional[LedBrightness]: """LED brightness if available.""" if self.data["led_b"] is not None: return LedBrightness(self.data["led_b"]) return None @property @setting( name="Child Lock", icon="mdi:lock", setter_name="set_child_lock", device_class="switch", entity_category="config", ) def child_lock(self) -> bool: """Return True if child lock is on.""" return self.data["child_lock"] == "on" @property def target_humidity(self) -> int: """Target humidity. Can be either 30, 40, 50, 60, 70, 80 percent. """ return self.data["limit_hum"] @property def trans_level(self) -> Optional[int]: """The meaning of the property is unknown. The property is used to determine the strong mode is enabled on old firmware. """ if "trans_level" in self.data and self.data["trans_level"] is not None: return self.data["trans_level"] return None @property def strong_mode_enabled(self) -> bool: if self.firmware_version_minor == 25: if self.trans_level == 90: return True elif self.firmware_version_minor > 25 or self.firmware_version_minor == 0: return self.mode.value == "strong" return False @property def firmware_version(self) -> str: """Returns the fw_ver of miIO.info. For example 1.2.9_5033. """ if self.device_info.firmware_version is None: return "missing fw version" return self.device_info.firmware_version @property def firmware_version_major(self) -> str: parts = self.firmware_version.rsplit("_", 1) return parts[0] @property def firmware_version_minor(self) -> int: parts = self.firmware_version.rsplit("_", 1) try: return int(parts[1]) except IndexError: return 0 @property @sensor( "Motor Speed", unit="rpm", device_class="measurement", icon="mdi:fast-forward", entity_category="diagnostic", ) def motor_speed(self) -> Optional[int]: """Current motor speed.""" if "speed" in self.data and self.data["speed"] is not None: return self.data["speed"] return None @property def depth(self) -> Optional[int]: """Return raw value of depth.""" _LOGGER.warning( "The 'depth' property is deprecated and will be removed in the future. Use 'water_level' and 'water_tank_attached' properties instead." ) if "depth" in self.data: return self.data["depth"] return None @property @sensor( "Water Level", unit="%", device_class="measurement", icon="mdi:water-check", entity_category="diagnostic", ) def water_level(self) -> Optional[int]: """Return current water level in percent. If water tank is full, depth is 120. If water tank is overfilled, depth is 125. """ depth = self.data.get("depth") if depth is None or depth > 125: return None if depth < 0: return 0 return int(min(depth / 1.2, 100)) @property @sensor( "Water Tank Attached", device_class="connectivity", icon="mdi:car-coolant-level", entity_category="diagnostic", ) def water_tank_attached(self) -> Optional[bool]: """True if the water tank is attached. If water tank is detached, depth is 127. """ if self.data.get("depth") is not None: return self.data["depth"] != 127 return None @property def water_tank_detached(self) -> Optional[bool]: """True if the water tank is detached. If water tank is detached, depth is 127. """ _LOGGER.warning( "The 'water_tank_detached' property is deprecated and will be removed in the future. Use 'water_tank_attached' properties instead." ) if self.data.get("depth") is not None: return self.data["depth"] == 127 return None @property @setting( name="Dry Mode", icon="mdi:hair-dryer", setter_name="set_dry", device_class="switch", entity_category="config", ) def dry(self) -> Optional[bool]: """Dry mode: The amount of water is not enough to continue to work for about 8 hours. Return True if dry mode is on if available. """ if "dry" in self.data and self.data["dry"] is not None: return self.data["dry"] == "on" return None @property @sensor( "Use Time", unit="s", device_class="total_increasing", icon="mdi:progress-clock", entity_category="diagnostic", ) def use_time(self) -> Optional[int]: """How long the device has been active in seconds.""" return self.data["use_time"] @property def hardware_version(self) -> Optional[str]: """The hardware version.""" return self.data["hw_version"] @property def button_pressed(self) -> Optional[str]: """Last pressed button.""" if "button_pressed" in self.data and self.data["button_pressed"] is not None: return self.data["button_pressed"] return None
[docs] class AirHumidifier(Device): """Implementation of Xiaomi Mi Air Humidifier.""" _supported_models = SUPPORTED_MODELS
[docs] @command() def status(self) -> AirHumidifierStatus: """Retrieve properties.""" properties = AVAILABLE_PROPERTIES.get( self.model, AVAILABLE_PROPERTIES[MODEL_HUMIDIFIER_V1] ) # A single request is limited to 16 properties. Therefore the # properties are divided into multiple requests _props_per_request = 15 # The CA1, CB1 and CB2 are limited to a single property per request if self.model in [ MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_CB1, MODEL_HUMIDIFIER_CB2, ]: _props_per_request = 1 values = self.get_properties(properties, max_properties=_props_per_request) return AirHumidifierStatus( defaultdict(lambda: None, zip(properties, values)), self.info() )
[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.""" try: return self.send("set_mode", [mode.value]) except DeviceError as error: # {'code': -6011, 'message': 'device_poweroff'} if error.code == -6011: self.on() return self.send("set_mode", [mode.value]) raise
[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.""" if self.model in [MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_CB1]: return self.send("set_led_b", [str(brightness.value)]) 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.set_led_brightness(LedBrightness.Bright) else: return self.set_led_brightness(LedBrightness.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("humidity", type=int), default_output=format_output("Setting target humidity to {humidity}"), ) def set_target_humidity(self, humidity: int): """Set the target humidity.""" if humidity not in [30, 40, 50, 60, 70, 80]: raise ValueError("Invalid target humidity: %s" % humidity) return self.send("set_limit_hum", [humidity])
[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.""" if dry: return self.send("set_dry", ["on"]) else: return self.send("set_dry", ["off"])