Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Xiaomi Airfresh VA2 support #360

Merged
merged 5 commits into from
Aug 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Supported devices
- Xiaomi Smart WiFi Speaker (:class:`miio.wifispeaker`) (incomplete, please `feel free to help improve the support <https://github.com/rytilahti/python-miio/issues/69>`__)
- Xiaomi Mi WiFi Repeater 2 (:class:`miio.wifirepeater`)
- Xiaomi Mi Smart Rice Cooker (:class:`miio.cooker`)
- Xiaomi Smartmi Fresh Air System (:class:`miio.airfresh`)
- Yeelight light bulbs (:class:`miio.yeelight`) (only a very rudimentary support, use `python-yeelight <https://gitlab.com/stavros/python-yeelight/>`__ for a more complete support)

*Feel free to create a pull request to add support for new devices as
Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# flake8: noqa
from miio.airconditioningcompanion import AirConditioningCompanion
from miio.airfresh import AirFresh
from miio.airhumidifier import AirHumidifier
from miio.airpurifier import AirPurifier
from miio.airqualitymonitor import AirQualityMonitor
Expand Down
293 changes: 293 additions & 0 deletions miio/airfresh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import enum
import logging
from collections import defaultdict
from typing import Any, Dict, Optional

import click

from .click_common import command, format_output, EnumType
from .device import Device, DeviceException

_LOGGER = logging.getLogger(__name__)


class AirFreshException(DeviceException):
pass


class OperationMode(enum.Enum):
# Supported modes of the Air Fresh VA2 (zhimi.airfresh.va2)
Auto = 'auto'
Silent = 'silent'
Interval = 'interval'
Low = 'low'
Middle = 'middle'
Strong = 'strong'


class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2


class AirFreshStatus:
"""Container for status reports from the air fresh."""

def __init__(self, data: Dict[str, Any]) -> None:
self.data = data

@property
def power(self) -> str:
"""Power state."""
return self.data["power"]

@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.power == "on"

@property
def aqi(self) -> int:
"""Air quality index."""
return self.data["aqi"]

@property
def average_aqi(self) -> int:
"""Average of the air quality index."""
return self.data["average_aqi"]

@property
def co2(self) -> int:
"""Carbon dioxide."""
return self.data["co2"]

@property
def humidity(self) -> int:
"""Current humidity."""
return self.data["humidity"]

@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if self.data["temp_dec"] is not None:
return self.data["temp_dec"] / 10.0

return None

@property
def mode(self) -> OperationMode:
"""Current operation mode."""
return OperationMode(self.data["mode"])

@property
def led_brightness(self) -> Optional[LedBrightness]:
"""Brightness of the LED."""
if self.data["led_level"] is not None:
try:
return LedBrightness(self.data["led_level"])
except ValueError:
_LOGGER.error("Unsupported LED brightness discarded: %s", self.data["led_level"])
return None

return None

@property
def buzzer(self) -> Optional[bool]:
"""Return True if buzzer is on."""
if self.data["buzzer"] is not None:
return self.data["buzzer"] == "on"

return None

@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"] == "on"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this and the other properties also be protected against None?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property is always available and doesn't need to be safeguarded.


@property
def filter_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["filter_life"]

@property
def filter_hours_used(self) -> int:
"""How long the filter has been in use."""
return self.data["f1_hour_used"]

@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]

@property
def motor_speed(self) -> int:
"""Speed of the motor."""
return self.data["motor1_speed"]

@property
def extra_features(self) -> Optional[int]:
return self.data["app_extra"]

def __repr__(self) -> str:
s = "<AirFreshStatus power=%s, " \
"aqi=%s, " \
"average_aqi=%s, " \
"temperature=%s, " \
"humidity=%s%%, " \
"co2=%s, " \
"mode=%s, " \
"led_brightness=%s, " \
"buzzer=%s, " \
"child_lock=%s, " \
"filter_life_remaining=%s, " \
"filter_hours_used=%s, " \
"use_time=%s, " \
"motor_speed=%s, " \
"extra_features=%s>" % \
(self.power,
self.aqi,
self.average_aqi,
self.temperature,
self.humidity,
self.co2,
self.mode,
self.led_brightness,
self.buzzer,
self.child_lock,
self.filter_life_remaining,
self.filter_hours_used,
self.use_time,
self.motor_speed,
self.extra_features)
return s

def __json__(self):
return self.data


class AirFresh(Device):
"""Main class representing the air fresh."""

@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"AQI: {result.aqi} μg/m³\n"
"Average AQI: {result.average_aqi} μg/m³\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"CO2: {result.co2} %\n"
"Mode: {result.mode.value}\n"
"LED brightness: {result.led_brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Filter life remaining: {result.filter_life_remaining} %\n"
"Filter hours used: {result.filter_hours_used}\n"
"Use time: {result.use_time} s\n"
"Motor speed: {result.motor_speed} rpm\n"
)
)
def status(self) -> AirFreshStatus:
"""Retrieve properties."""

properties = ["power", "temp_dec", "aqi", "average_aqi", "co2", "buzzer", "child_lock",
"humidity", "led_level", "mode", "motor1_speed", "use_time",
"ntcT", "app_extra", "f1_hour_used", "filter_life", "f_hour",
"favorite_level", "led"]

# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:15]))
_props[:] = _props[15:]

properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count, values_count)

return AirFreshStatus(
defaultdict(lambda: None, zip(properties, values)))

@command(
default_output=format_output("Powering on"),
)
def on(self):
"""Power on."""
return self.send("set_power", ["on"])

@command(
default_output=format_output("Powering off"),
)
def off(self):
"""Power off."""
return self.send("set_power", ["off"])

@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'")
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.send("set_mode", [mode.value])

@command(
click.argument("brightness", type=EnumType(LedBrightness, False)),
default_output=format_output(
"Setting LED brightness to {brightness}")
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
return self.send("set_led_level", [brightness.value])

@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"])

@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"])

@command(
click.argument("value", type=int),
default_output=format_output("Setting extra to {value}")
)
def set_extra_features(self, value: int):
"""Storage register to enable extra features at the app."""
if value < 0:
raise AirFreshException("Invalid app extra value: %s" % value)

return self.send("set_app_extra", [value])

@command(
default_output=format_output("Resetting filter")
)
def reset_filter(self):
"""Resets filter hours used and remaining life."""
return self.send('reset_filter1')
3 changes: 2 additions & 1 deletion miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import zeroconf

from . import (Device, Vacuum, ChuangmiPlug, PowerStrip, AirPurifier, Ceil,
from . import (Device, Vacuum, ChuangmiPlug, PowerStrip, AirPurifier, AirFresh, Ceil,
PhilipsBulb, PhilipsEyecare, ChuangmiIr, AirHumidifier,
WaterPurifier, WifiSpeaker, WifiRepeater, Yeelight, Fan, Cooker,
AirConditioningCompanion)
Expand Down Expand Up @@ -68,6 +68,7 @@
"zhimi-fan-v2": partial(Fan, model=MODEL_FAN_V2),
"zhimi-fan-v3": partial(Fan, model=MODEL_FAN_V3),
"zhimi-fan-sa1": partial(Fan, model=MODEL_FAN_SA1),
"zhimi-airfresh-va2": AirFresh,
"lumi-gateway-": lambda x: other_package_info(
x, "https://github.com/Danielhiversen/PyXiaomiGateway")
} # type: Dict[str, Union[Callable, Device]]
Expand Down
Loading