-
-
Notifications
You must be signed in to change notification settings - Fork 554
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
Device support for the xiaomi air purifier added. #31
Changes from 12 commits
ef60009
f1c03c3
fa9b0e7
bad603f
d8f5920
6191b80
19fc11e
7b990a2
7933690
1b3a934
7ae0247
72a2086
5409359
8085496
61b5189
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
__pycache__ | ||
.idea/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
from .device import Device | ||
from typing import Any, Dict | ||
import enum | ||
|
||
|
||
class OperationMode(enum.Enum): | ||
Auto = 'auto' | ||
Silent = 'silent' | ||
Favorite = 'favorite' | ||
Medium = 'medium' | ||
High = 'high' | ||
Strong = 'strong' | ||
Idle = 'idle' | ||
|
||
|
||
class LedBrightness(enum.Enum): | ||
Bright = 0 | ||
Dim = 1 | ||
Off = 2 | ||
|
||
|
||
class AirPurifier(Device): | ||
"""Main class representing the air purifier.""" | ||
|
||
def status(self): | ||
"""Retrieve properties.""" | ||
|
||
# A few more properties: | ||
properties = ['power', 'aqi', 'humidity', 'temp_dec', | ||
'mode', 'led', 'led_b', 'buzzer', 'child_lock', | ||
'limit_hum', 'trans_level', 'bright', | ||
'favorite_level', 'filter1_life', 'act_det', | ||
'f1_hour_used', 'use_time', 'motor1_speed'] | ||
|
||
values = self.send( | ||
"get_prop", | ||
properties | ||
) | ||
return AirPurifierStatus(dict(zip(properties, values))) | ||
|
||
def set_mode(self, mode: OperationMode): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undefined name 'OperationMode' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's better to move the class on top of the file, another possibility would be to use a forward declaration ('OperationMode'), which does not look so nice IMO and should be avoided where not really necessary (like when using it inside its own defining class). |
||
"""Set mode.""" | ||
|
||
# auto, silent, favorite, medium, high, strong, idle | ||
return self.send("set_mode", [mode.value]) | ||
|
||
def set_favorite_level(self, level: int): | ||
"""Set favorite level.""" | ||
|
||
# Set the favorite level used when the mode is `favorite`, | ||
# should be between 0 and 16. | ||
return self.send("favorite_level", [level]) # 0 ... 16 | ||
|
||
def set_led_brightness(self, brightness: LedBrightness): | ||
"""Set led brightness.""" | ||
|
||
# bright: 0, dim: 1, off: 2 | ||
return self.send("set_led_b", [brightness.value]) | ||
|
||
def set_led(self, led: bool): | ||
"""Turn led on/off.""" | ||
if led: | ||
return self.send("set_led", ['on']) | ||
else: | ||
return self.send("set_led", ['off']) | ||
|
||
def set_buzzer(self, buzzer: bool): | ||
"""Set buzzer.""" | ||
if buzzer: | ||
return self.send("set_mode", ["on"]) | ||
else: | ||
return self.send("set_mode", ["off"]) | ||
|
||
def set_humidity_limit(self, limit: int): | ||
"""Set humidity limit.""" | ||
|
||
# 40, 50, 60, 70 or 80 | ||
return self.send("set_limit_hum", [limit]) | ||
|
||
|
||
class AirPurifierStatus: | ||
"""Container for status reports from the air purifier.""" | ||
def __init__(self, data: Dict[str, Any]) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undefined name 'Dict' |
||
# Response of a Air Purifier Pro: | ||
# ['power': 'off', 'aqi': 41, 'humidity': 62, 'temp_dec': 293, | ||
# 'mode': 'auto', 'led': 'on', 'led_b': null, 'buzzer': null, | ||
# 'child_lock': 'off', 'limit_hum': null, 'trans_level': null, | ||
# 'bright': 71, 'favorite_level': 17, 'filter1_life': 77, | ||
# 'act_det': null, 'f1_hour_used': 771, 'use_time': 2776200, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Favorite level is 17 here, but in the comment of it says: # Favorite level used when the mode is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, to add. Those are probably encoded as Nones, trying to construct an enum out of them will cause ValueError, so trying to access
is probably the best solution here. |
||
# 'motor1_speed': 0] | ||
self.data = data | ||
|
||
@property | ||
def power(self) -> str: | ||
return self.data["power"] | ||
|
||
@property | ||
def is_on(self) -> bool: | ||
return self.power == "on" | ||
|
||
@property | ||
def aqi(self) -> int: | ||
return self.data["aqi"] | ||
|
||
@property | ||
def humidity(self) -> int: | ||
return self.data["humidity"] | ||
|
||
@property | ||
def temperature(self) -> float: | ||
return self.data["temp_dec"] / 10.0 | ||
|
||
@property | ||
def mode(self) -> OperationMode: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undefined name 'OperationMode' |
||
return OperationMode(self.data["mode"]) | ||
|
||
@property | ||
def led(self) -> bool: | ||
return self.data["led"] == "on" | ||
|
||
@property | ||
def led_brightness(self) -> LedBrightness: | ||
return LedBrightness(self.data["led_b"]) | ||
|
||
@property | ||
def buzzer(self) -> bool: | ||
return self.data["buzzer"] == "on" | ||
|
||
@property | ||
def child_lock(self) -> bool: | ||
return self.data["child_lock"] == "on" | ||
|
||
@property | ||
def humidity_limit(self) -> int: | ||
return self.data["limit_hum"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think my comment on these got lost somewhere, can we have better, more descriptive names for this, trans_level, act_det and led_b? led_brightness probably for led_b, but I have no idea what those two others could even be.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I don't know the meaning of trans_level and act_det, too. ;-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. str or float/int? |
||
|
||
@property | ||
def trans_level(self) -> str: | ||
return self.data["trans_level"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Level indicates that it's probably an integer, but this is ok and can be tuned when the payload is known.. |
||
|
||
@property | ||
def bright(self) -> int: | ||
return self.data["bright"] | ||
|
||
@property | ||
def favorite_level(self) -> int: | ||
# Favorite level used when the mode is `favorite`. Between 0 and 16. | ||
return self.data["favorite_level"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Considering the setter is using int, this is probably integer too, right? |
||
|
||
@property | ||
def filter_life_remaining(self) -> int: | ||
return self.data["filter1_life"] | ||
|
||
@property | ||
def act_det(self) -> bool: | ||
return self.data["act_det"] == "on" | ||
|
||
@property | ||
def filter_hours_used(self) -> int: | ||
return self.data["f1_hour_used"] | ||
|
||
@property | ||
def use_time(self) -> int: | ||
return self.data["use_time"] | ||
|
||
@property | ||
def motor_speed(self) -> int: | ||
return self.data["motor1_speed"] | ||
|
||
def __str__(self) -> str: | ||
s = "<AirPurifierStatus power=%s, aqi=%s temperature=%s%%, " \ | ||
"humidity=%s%% mode=%s%%, led=%s%%, " \ | ||
"led_brightness=%s%% buzzer=%s%%, " \ | ||
"child_lock=%s%%, humidity_limit=%s%%, trans_level=%s%%, " \ | ||
"bright=%s%%, favorite_level=%s%%, filter_life_remaining=%s%%, " \ | ||
"act_det=%s%%, filter_hours_used=%s%%, use_time=%s%%, " \ | ||
"motor_speed=%s%%>" % \ | ||
(self.power, self.aqi, self.temperature, | ||
self.humidity, self.mode, self.led, | ||
self.led_brightness, self.buzzer, | ||
self.child_lock, self.humidity_limit, self.trans_level, | ||
self.bright, self.favorite_level, self.filter_life_remaining, | ||
self.act_det, self.filter_hours_used, self.use_time, | ||
self.motor_speed) | ||
return s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering that the get_prop is pretty generic (it's also used for the strip it seems, as well as for the yeelights), maybe it makes sense to have a list (or a mapping?) of properties and their corresponding types listed and move the status() implementation to the parent class at some point? See also the comments below regarding to property names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the properties are somehow generic but they cannot be applied to all devices. For example there are three versions of the air purifier out there all of them with different supported OperationModes. Some devices does respond with "on" and "off"... others with "true" and "false". I suggest to refactor the code if the support is stable and the knowledge about different responses stronger.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, sounds sane! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To add, do you happen to have an example also for this case? It would be nice to have example answers for all calls supported by devices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I must confess I don't own the device. I just want to see it supported. I will ask around. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for helping to get it supported! :-) Potential tester available here: https://community.home-assistant.io/t/xiaomi-mi-air-purifier-support/14228/12 but for that we need to have a simple test script.