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

Device support for the xiaomi air purifier added. #31

Merged
merged 15 commits into from Aug 14, 2017
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
@@ -1 +1,2 @@
__pycache__
.idea/
1 change: 1 addition & 0 deletions mirobo/__init__.py
Expand Up @@ -3,5 +3,6 @@
from mirobo.containers import VacuumStatus, ConsumableStatus, CleaningDetails, CleaningSummary, Timer
from mirobo.vacuum import Vacuum, VacuumException
from mirobo.plug import Plug
from mirobo.airpurifier import AirPurifier
from mirobo.strip import Strip
from mirobo.device import Device, DeviceException
183 changes: 183 additions & 0 deletions mirobo/airpurifier.py
@@ -0,0 +1,183 @@
from .device import Device
from typing import Any, Dict
import enum


class OperationMode(enum.Enum):
Auto = 'auto'
Silent = 'silent'
Favorite = 'favorite'
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
)
Copy link
Owner

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.

Copy link
Collaborator Author

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.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, sounds sane! 👍

Copy link
Owner

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.

Copy link
Collaborator Author

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. :-)

Copy link
Owner

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.

return AirPurifierStatus(dict(zip(properties, values)))

def set_mode(self, mode: OperationMode):

Choose a reason for hiding this comment

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

undefined name 'OperationMode'

Copy link
Owner

Choose a reason for hiding this comment

The 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, 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:

Choose a reason for hiding this comment

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

undefined name 'Dict'
undefined name 'Any'

# 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,
Copy link
Owner

Choose a reason for hiding this comment

The 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 favorite. Between 0 and 16. Those which return null should probably be marked in typing with eg. Optional[bool] (buzzer for example). Hopefully we can get some more test examples to see if those are always null..

Copy link
Owner

@rytilahti rytilahti Jul 27, 2017

Choose a reason for hiding this comment

The 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 led_brightness would cause a crash at least. I think

if self.data["led_b"] is not None:
    return LedBrightness(self.data["led_b"])

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:

Choose a reason for hiding this comment

The 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:
if self.data["led_b"] is not None:
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"]

@property
def trans_level(self) -> str:
return self.data["trans_level"]
Copy link
Owner

Choose a reason for hiding this comment

The 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"]

@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