Source code for miio.integrations.zhimi.fan.zhimi_miot

import enum
from typing import Any, Dict

import click

from miio import DeviceException, DeviceStatus, MiotDevice
from miio.click_common import EnumType, command, format_output
from miio.utils import deprecated


[docs] class OperationMode(enum.Enum): Normal = "normal" Nature = "nature"
[docs] class MoveDirection(enum.Enum): Left = "left" Right = "right"
MODEL_FAN_ZA5 = "zhimi.fan.za5" MIOT_MAPPING = { MODEL_FAN_ZA5: { # https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:fan:0000A005:zhimi-za5:1 "power": {"siid": 2, "piid": 1}, "fan_level": {"siid": 2, "piid": 2}, "swing_mode": {"siid": 2, "piid": 3}, "swing_mode_angle": {"siid": 2, "piid": 5}, "mode": {"siid": 2, "piid": 7}, "power_off_time": {"siid": 2, "piid": 10}, "anion": {"siid": 2, "piid": 11}, "child_lock": {"siid": 3, "piid": 1}, "light": {"siid": 4, "piid": 3}, "buzzer": {"siid": 5, "piid": 1}, "buttons_pressed": {"siid": 6, "piid": 1}, "battery_supported": {"siid": 6, "piid": 2}, "set_move": {"siid": 6, "piid": 3}, "speed_rpm": {"siid": 6, "piid": 4}, "powersupply_attached": {"siid": 6, "piid": 5}, "fan_speed": {"siid": 6, "piid": 8}, "humidity": {"siid": 7, "piid": 1}, "temperature": {"siid": 7, "piid": 7}, }, } SUPPORTED_ANGLES = { MODEL_FAN_ZA5: [30, 60, 90, 120], }
[docs] class OperationModeFanZA5(enum.Enum): Nature = 0 Normal = 1
[docs] class FanStatusZA5(DeviceStatus): """Container for status reports for FanZA5.""" def __init__(self, data: Dict[str, Any]) -> None: """Response of FanZA5 (zhimi.fan.za5): {'code': -4005, 'did': 'set_move', 'piid': 3, 'siid': 6}, {'code': 0, 'did': 'anion', 'piid': 11, 'siid': 2, 'value': True}, {'code': 0, 'did': 'battery_supported', 'piid': 2, 'siid': 6, 'value': False}, {'code': 0, 'did': 'buttons_pressed', 'piid': 1, 'siid': 6, 'value': 0}, {'code': 0, 'did': 'buzzer', 'piid': 1, 'siid': 5, 'value': False}, {'code': 0, 'did': 'child_lock', 'piid': 1, 'siid': 3, 'value': False}, {'code': 0, 'did': 'fan_level', 'piid': 2, 'siid': 2, 'value': 4}, {'code': 0, 'did': 'fan_speed', 'piid': 8, 'siid': 6, 'value': 100}, {'code': 0, 'did': 'humidity', 'piid': 1, 'siid': 7, 'value': 55}, {'code': 0, 'did': 'light', 'piid': 3, 'siid': 4, 'value': 100}, {'code': 0, 'did': 'mode', 'piid': 7, 'siid': 2, 'value': 0}, {'code': 0, 'did': 'power', 'piid': 1, 'siid': 2, 'value': False}, {'code': 0, 'did': 'power_off_time', 'piid': 10, 'siid': 2, 'value': 0}, {'code': 0, 'did': 'powersupply_attached', 'piid': 5, 'siid': 6, 'value': True}, {'code': 0, 'did': 'speed_rpm', 'piid': 4, 'siid': 6, 'value': 0}, {'code': 0, 'did': 'swing_mode', 'piid': 3, 'siid': 2, 'value': True}, {'code': 0, 'did': 'swing_mode_angle', 'piid': 5, 'siid': 2, 'value': 60}, {'code': 0, 'did': 'temperature', 'piid': 7, 'siid': 7, 'value': 26.4}, """ self.data = data @property def ionizer(self) -> bool: """True if negative ions generation is enabled.""" return self.data["anion"] @property def battery_supported(self) -> bool: """True if battery is supported.""" return self.data["battery_supported"] @property def buttons_pressed(self) -> str: """What buttons on the fan are pressed now.""" code = self.data["buttons_pressed"] if code == 0: return "None" if code == 1: return "Power" if code == 2: return "Swing" return "Unknown" @property def buzzer(self) -> bool: """True if buzzer is turned on.""" return self.data["buzzer"] @property def child_lock(self) -> bool: """True if child lock if on.""" return self.data["child_lock"] @property def fan_level(self) -> int: """Fan level (1-4).""" return self.data["fan_level"] @property # type: ignore @deprecated("Use speed()") def fan_speed(self) -> int: """Fan speed (1-100).""" return self.speed @property def speed(self) -> int: """Fan speed (1-100).""" return self.data["fan_speed"] @property def humidity(self) -> int: """Air humidity in percent.""" return self.data["humidity"] @property def led_brightness(self) -> int: """LED brightness (1-100).""" return self.data["light"] @property def mode(self) -> OperationMode: """Operation mode (normal or nature).""" return OperationMode[OperationModeFanZA5(self.data["mode"]).name] @property def power(self) -> str: """Power state.""" return "on" if self.data["power"] else "off" @property def is_on(self) -> bool: """True if device is currently on.""" return self.data["power"] @property def delay_off_countdown(self) -> int: """Countdown until turning off in minutes.""" return self.data["power_off_time"] @property def powersupply_attached(self) -> bool: """True is power supply is attached.""" return self.data["powersupply_attached"] @property def speed_rpm(self) -> int: """Fan rotations per minute.""" return self.data["speed_rpm"] @property def oscillate(self) -> bool: """True if oscillation is enabled.""" return self.data["swing_mode"] @property def angle(self) -> int: """Oscillation angle.""" return self.data["swing_mode_angle"] @property def temperature(self) -> Any: """Air temperature (degree celsius).""" return self.data["temperature"]
[docs] class FanZA5(MiotDevice): _mappings = MIOT_MAPPING
[docs] @command( default_output=format_output( "", "Angle: {result.angle}\n" "Battery Supported: {result.battery_supported}\n" "Buttons Pressed: {result.buttons_pressed}\n" "Buzzer: {result.buzzer}\n" "Child Lock: {result.child_lock}\n" "Delay Off Countdown: {result.delay_off_countdown}\n" "Fan Level: {result.fan_level}\n" "Fan Speed: {result.fan_speed}\n" "Humidity: {result.humidity}\n" "Ionizer: {result.ionizer}\n" "LED Brightness: {result.led_brightness}\n" "Mode: {result.mode.name}\n" "Oscillate: {result.oscillate}\n" "Power: {result.power}\n" "Powersupply Attached: {result.powersupply_attached}\n" "Speed RPM: {result.speed_rpm}\n" "Temperature: {result.temperature}\n", ) ) def status(self) -> FanStatusZA5: """Retrieve properties.""" return FanStatusZA5( { 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("on", type=bool), default_output=format_output( lambda on: "Turning on ionizer" if on else "Turning off ionizer" ), ) def set_ionizer(self, on: bool): """Set ionizer on/off.""" return self.set_property("anion", on)
[docs] @command( click.argument("speed", type=int), default_output=format_output("Setting speed to {speed}%"), ) def set_speed(self, speed: int): """Set fan speed.""" if speed < 1 or speed > 100: raise ValueError("Invalid speed: %s" % speed) return self.set_property("fan_speed", speed)
[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 not in SUPPORTED_ANGLES[self.model]: raise ValueError( "Unsupported angle. Supported values: " + ", ".join(f"{i}" for i in SUPPORTED_ANGLES[self.model]) ) return self.set_property("swing_mode_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.""" return self.set_property("swing_mode", oscillate)
[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("brightness", type=int), default_output=format_output("Setting LED brightness to {brightness}%"), ) def set_led_brightness(self, brightness: int): """Set LED brightness.""" if brightness < 0 or brightness > 100: raise ValueError("Invalid brightness: %s" % brightness) return self.set_property("light", brightness)
[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.set_property("mode", OperationModeFanZA5[mode.name].value)
[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 or seconds > 10 * 60 * 60: raise ValueError("Invalid value for a delayed turn off: %s" % seconds) return self.set_property("power_off_time", seconds)
[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 fan 7.5 degrees horizontally to given direction.""" status = self.status() if status.oscillate: raise DeviceException( "Rotation requires oscillation to be turned off to function." ) return self.set_property("set_move", direction.name.lower())