import enum
import logging
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_HEATER_ZA1 = "zhimi.heater.za1"
MODEL_HEATER_MA1 = "zhimi.elecheater.ma1"
AVAILABLE_PROPERTIES_COMMON = [
"power",
"target_temperature",
"brightness",
"buzzer",
"child_lock",
"temperature",
"use_time",
]
AVAILABLE_PROPERTIES_ZA1 = ["poweroff_time", "relative_humidity"]
AVAILABLE_PROPERTIES_MA1 = ["poweroff_level", "poweroff_value"]
SUPPORTED_MODELS: Dict[str, Dict[str, Any]] = {
MODEL_HEATER_ZA1: {
"available_properties": AVAILABLE_PROPERTIES_COMMON + AVAILABLE_PROPERTIES_ZA1,
"temperature_range": (16, 32),
"delay_off_range": (0, 9 * 3600),
},
MODEL_HEATER_MA1: {
"available_properties": AVAILABLE_PROPERTIES_COMMON + AVAILABLE_PROPERTIES_MA1,
"temperature_range": (20, 32),
"delay_off_range": (0, 5 * 3600),
},
}
[docs]
class Brightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
[docs]
class HeaterStatus(DeviceStatus):
"""Container for status reports from the Smartmi Zhimi Heater."""
def __init__(self, data: Dict[str, Any]) -> None:
"""Response of a Heater (zhimi.heater.za1):
{'power': 'off', 'target_temperature': 24, 'brightness': 1,
'buzzer': 'on', 'child_lock': 'off', 'temperature': 22.3,
'use_time': 43117, 'poweroff_time': 0, 'relative_humidity': 34}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""True if device is currently on."""
return self.power == "on"
@property
def humidity(self) -> Optional[int]:
"""Current humidity."""
if (
"relative_humidity" in self.data
and self.data["relative_humidity"] is not None
):
return self.data["relative_humidity"]
return None
@property
def temperature(self) -> float:
"""Current temperature."""
return self.data["temperature"]
@property
def target_temperature(self) -> int:
"""Target temperature."""
return self.data["target_temperature"]
@property
def brightness(self) -> Brightness:
"""Display brightness."""
return Brightness(self.data["brightness"])
@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["buzzer"] in ["on", 1, 2]
@property
def child_lock(self) -> bool:
"""True if child lock is on."""
return self.data["child_lock"] == "on"
@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
def delay_off_countdown(self) -> Optional[int]:
"""Countdown until turning off in seconds."""
if "poweroff_time" in self.data and self.data["poweroff_time"] is not None:
return self.data["poweroff_time"]
if "poweroff_level" in self.data and self.data["poweroff_level"] is not None:
return self.data["poweroff_level"]
return None
[docs]
class Heater(Device):
"""Main class representing the Smartmi Zhimi Heater."""
_supported_models = list(SUPPORTED_MODELS.keys())
[docs]
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Target temperature: {result.target_temperature} °C\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"Display brightness: {result.brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Power-off time: {result.delay_off_countdown}\n",
)
)
def status(self) -> HeaterStatus:
"""Retrieve properties."""
properties = SUPPORTED_MODELS.get(
self.model, SUPPORTED_MODELS[MODEL_HEATER_ZA1]
)["available_properties"]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props_per_request = 15
# The MA1, ZA1 is limited to a single property per request
if self.model in [MODEL_HEATER_MA1, MODEL_HEATER_ZA1]:
_props_per_request = 1
values = self.get_properties(properties, max_properties=_props_per_request)
return HeaterStatus(dict(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("temperature", type=int),
default_output=format_output("Setting target temperature to {temperature}"),
)
def set_target_temperature(self, temperature: int):
"""Set target temperature."""
min_temp: int
max_temp: int
min_temp, max_temp = SUPPORTED_MODELS.get(
self.model, SUPPORTED_MODELS[MODEL_HEATER_ZA1]
)["temperature_range"]
if not min_temp <= temperature <= max_temp:
raise ValueError("Invalid target temperature: %s" % temperature)
return self.send("set_target_temperature", [temperature])
[docs]
@command(
click.argument("brightness", type=EnumType(Brightness)),
default_output=format_output("Setting display brightness to {brightness}"),
)
def set_brightness(self, brightness: Brightness):
"""Set display brightness."""
return self.send("set_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."""
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("seconds", type=int),
default_output=format_output("Setting delayed turn off to {seconds} seconds"),
)
def delay_off(self, seconds: int):
"""Set delay off seconds."""
min_delay: int
max_delay: int
min_delay, max_delay = SUPPORTED_MODELS.get(
self.model, SUPPORTED_MODELS[MODEL_HEATER_ZA1]
)["delay_off_range"]
if not min_delay <= seconds <= max_delay:
raise ValueError("Invalid delay time: %s" % seconds)
if self.model == MODEL_HEATER_ZA1:
return self.send("set_poweroff_time", [seconds])
elif self.model == MODEL_HEATER_MA1:
return self.send("set_poweroff_level", [seconds // 3600])
return None