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
from miio.devicestatus import sensor, setting
[docs]
class MoveDirection(enum.Enum):
Left = "left"
Right = "right"
[docs]
class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
_LOGGER = logging.getLogger(__name__)
MODEL_FAN_V2 = "zhimi.fan.v2"
MODEL_FAN_V3 = "zhimi.fan.v3"
MODEL_FAN_SA1 = "zhimi.fan.sa1"
MODEL_FAN_ZA1 = "zhimi.fan.za1"
MODEL_FAN_ZA3 = "zhimi.fan.za3"
MODEL_FAN_ZA4 = "zhimi.fan.za4"
AVAILABLE_PROPERTIES_COMMON = [
"angle",
"speed",
"poweroff_time",
"power",
"ac_power",
"angle_enable",
"speed_level",
"natural_level",
"child_lock",
"buzzer",
"led_b",
"use_time",
]
AVAILABLE_PROPERTIES_COMMON_V2_V3 = [
"temp_dec",
"humidity",
"battery",
"bat_charge",
"button_pressed",
] + AVAILABLE_PROPERTIES_COMMON
AVAILABLE_PROPERTIES = {
MODEL_FAN_V3: AVAILABLE_PROPERTIES_COMMON_V2_V3,
MODEL_FAN_V2: ["led", "bat_state"] + AVAILABLE_PROPERTIES_COMMON_V2_V3,
MODEL_FAN_SA1: AVAILABLE_PROPERTIES_COMMON,
MODEL_FAN_ZA1: AVAILABLE_PROPERTIES_COMMON,
MODEL_FAN_ZA3: AVAILABLE_PROPERTIES_COMMON,
MODEL_FAN_ZA4: AVAILABLE_PROPERTIES_COMMON,
}
[docs]
class FanStatus(DeviceStatus):
"""Container for status reports from the Xiaomi Mi Smart Pedestal Fan."""
def __init__(self, data: Dict[str, Any]) -> None:
"""Response of a Fan (zhimi.fan.v3):
{'temp_dec': 232, 'humidity': 46, 'angle': 118, 'speed': 298,
'poweroff_time': 0, 'power': 'on', 'ac_power': 'off', 'battery': 98,
'angle_enable': 'off', 'speed_level': 1, 'natural_level': 0,
'child_lock': 'off', 'buzzer': 'on', 'led_b': 1, 'led': None,
'natural_enable': None, 'use_time': 0, 'bat_charge': 'complete',
'bat_state': None, 'button_pressed':'speed'}
Response of a Fan (zhimi.fan.sa1):
{'angle': 120, 'speed': 277, 'poweroff_time': 0, 'power': 'on',
'ac_power': 'on', 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 2,
'child_lock': 'off', 'buzzer': 0, 'led_b': 0, 'use_time': 2318}
Response of a Fan (zhimi.fan.sa4):
{'angle': 120, 'speed': 327, 'poweroff_time': 0, 'power': 'on',
'ac_power': 'on', 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 0,
'child_lock': 'off', 'buzzer': 2, 'led_b': 0, 'use_time': 85}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
@setting("Power", setter_name="set_power")
def is_on(self) -> bool:
"""True if device is currently on."""
return self.power == "on"
@property
@sensor("Humidity")
def humidity(self) -> Optional[int]:
"""Current humidity."""
if "humidity" in self.data and self.data["humidity"] is not None:
return self.data["humidity"]
return None
@property
@sensor("Temperature", unit="C")
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
return None
@property
@setting("LED", setter_name="set_led")
def led(self) -> Optional[bool]:
"""True if LED is turned on, if available."""
if "led" in self.data and self.data["led"] is not None:
return self.data["led"] == "on"
return None
@property
@setting("LED Brightness", choices=LedBrightness, setter_name="set_led_brightness")
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("Buzzer", setter_name="set_buzzer")
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["buzzer"] in ["on", 1, 2]
@property
@setting("Child Lock", setter_name="set_child_lock")
def child_lock(self) -> bool:
"""True if child lock is on."""
return self.data["child_lock"] == "on"
@property
@setting("Natural Speed Level", setter_name="set_natural_speed", max_value=100)
def natural_speed(self) -> Optional[int]:
"""Speed level in natural mode."""
if "natural_level" in self.data and self.data["natural_level"] is not None:
return self.data["natural_level"]
return None
@property
@setting("Direct Speed", setter_name="set_direct_speed", max_value=100)
def direct_speed(self) -> Optional[int]:
"""Speed level in direct mode."""
if "speed_level" in self.data and self.data["speed_level"] is not None:
return self.data["speed_level"]
return None
@property
@setting("Oscillate", setter_name="set_oscillate")
def oscillate(self) -> bool:
"""True if oscillation is enabled."""
return self.data["angle_enable"] == "on"
@property
@sensor("Battery", unit="%")
def battery(self) -> Optional[int]:
"""Current battery level."""
if "battery" in self.data and self.data["battery"] is not None:
return self.data["battery"]
return None
@property
@sensor("Battery Charge State")
def battery_charge(self) -> Optional[str]:
"""State of the battery charger, if available."""
if "bat_charge" in self.data and self.data["bat_charge"] is not None:
return self.data["bat_charge"]
return None
@property
@sensor("Battery State")
def battery_state(self) -> Optional[str]:
"""State of the battery, if available."""
if "bat_state" in self.data and self.data["bat_state"] is not None:
return self.data["bat_state"]
return None
@property
@sensor("AC Powered")
def ac_power(self) -> bool:
"""True if powered by AC."""
return self.data["ac_power"] == "on"
@property
def delay_off_countdown(self) -> int:
"""Countdown until turning off in seconds."""
return self.data["poweroff_time"]
@property
@sensor("Motor Speed", unit="RPM")
def speed(self) -> int:
"""Speed of the motor."""
return self.data["speed"]
@property
@setting("Oscillation Angle", setter_name="set_angle", max_value=120)
def angle(self) -> int:
"""Current angle."""
return self.data["angle"]
@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
@sensor("Last Pressed Button")
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 FanStatusZA4(FanStatus):
"""Container for status reports from the Xiaomi Mi Smart Pedestal Fan Zhimi ZA4."""
def __init__(self, data: Dict[str, Any]) -> None:
self.data = data
@property
def delay_off_countdown(self) -> int:
"""Countdown until turning off in minutes."""
return self.data["poweroff_time"] / 60
[docs]
class Fan(Device):
"""Main class representing the Xiaomi Mi Smart Pedestal Fan."""
_supported_models = list(AVAILABLE_PROPERTIES.keys())
[docs]
@command()
def status(self) -> FanStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props_per_request = 15
# The SA1, ZA1, ZA3 and ZA4 is limited to a single property per request
if self.model in [MODEL_FAN_SA1, MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4]:
_props_per_request = 1
values = self.get_properties(properties, max_properties=_props_per_request)
# The ZA4 has a countdown timer in minutes
if self.model in [MODEL_FAN_ZA4]:
return FanStatusZA4(dict(zip(properties, values)))
return FanStatus(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("power", type=bool),
)
def set_power(self, power: bool):
"""Turn device on or off."""
if power:
self.on()
else:
self.off()
[docs]
@command(
click.argument("speed", type=int),
default_output=format_output("Setting speed of the natural mode to {speed}"),
)
def set_natural_speed(self, speed: int):
"""Set natural level."""
if speed < 0 or speed > 100:
raise ValueError("Invalid speed: %s" % speed)
return self.send("set_natural_level", [speed])
[docs]
@command(
click.argument("speed", type=int),
default_output=format_output("Setting speed of the direct mode to {speed}"),
)
def set_direct_speed(self, speed: int):
"""Set speed of the direct mode."""
if speed < 0 or speed > 100:
raise ValueError("Invalid speed: %s" % speed)
return self.send("set_speed_level", [speed])
[docs]
@command(
click.argument("direction", type=EnumType(MoveDirection)),
default_output=format_output("Rotating the fan to the {direction}"),
)
def set_rotate(self, direction: MoveDirection):
"""Rotate the fan by -5/+5 degrees left/right."""
return self.send("set_move", [direction.value])
[docs]
@command(
click.argument("angle", type=int),
default_output=format_output("Setting angle to {angle}"),
)
def set_angle(self, angle: int):
"""Set the oscillation angle."""
if angle < 0 or angle > 120:
raise ValueError("Invalid angle: %s" % angle)
return self.send("set_angle", [angle])
[docs]
@command(
click.argument("oscillate", type=bool),
default_output=format_output(
lambda oscillate: (
"Turning on oscillate" if oscillate else "Turning off oscillate"
)
),
)
def set_oscillate(self, oscillate: bool):
"""Set oscillate on/off."""
if oscillate:
return self.send("set_angle_enable", ["on"])
else:
return self.send("set_angle_enable", ["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_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.
Not supported by model SA1.
"""
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 self.model in [MODEL_FAN_SA1, MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4]:
if buzzer:
return self.send("set_buzzer", [2])
else:
return self.send("set_buzzer", [0])
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."""
if seconds < 0:
raise ValueError("Invalid value for a delayed turn off: %s" % seconds)
# Set delay countdown in minutes for model ZA4
if self.model in [MODEL_FAN_ZA4]:
return self.send("set_poweroff_time", [seconds * 60])
return self.send("set_poweroff_time", [seconds])