-
-
Notifications
You must be signed in to change notification settings - Fork 561
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for deerma.humidifier.jsq{s,5} (#1193)
* Add support of the deerma.humidifier.jsqs Support of the deerma.humidifier.jsqs by example of the humidifier miot * Added AirHumidifierJsqs test * Fix lint issues * Move miio/airhumidifier_jsqs.py to miio/integrations/humidifier/deerma/ * Add _supported_models variable with model description * Fix lint issues * Add export of AirHumidifierJsqs in the deerma package * Support deerma.humidifier.jsq5 * Update README, support of Xiaomi Mi Smart Humidifier (jsqs, jsq5) Co-authored-by: Sebastian Muszynski <[email protected]>
- Loading branch information
Showing
8 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# flake8: noqa | ||
from .airhumidifier_jsqs import AirHumidifierJsqs |
235 changes: 235 additions & 0 deletions
235
miio/integrations/humidifier/deerma/airhumidifier_jsqs.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
import enum | ||
import logging | ||
from typing import Any, Dict, Optional | ||
|
||
import click | ||
|
||
from miio.click_common import EnumType, command, format_output | ||
from miio.exceptions import DeviceException | ||
from miio.miot_device import DeviceStatus, MiotDevice | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
_MAPPING = { | ||
# Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:humidifier:0000A00E:deerma-jsqs:2 | ||
# Air Humidifier (siid=2) | ||
"power": {"siid": 2, "piid": 1}, # bool | ||
"fault": {"siid": 2, "piid": 2}, # 0 | ||
"mode": {"siid": 2, "piid": 5}, # 1 - lvl1, 2 - lvl2, 3 - lvl3, 4 - auto | ||
"target_humidity": {"siid": 2, "piid": 6}, # [40, 80] step 1 | ||
# Environment (siid=3) | ||
"relative_humidity": {"siid": 3, "piid": 1}, # [0, 100] step 1 | ||
"temperature": {"siid": 3, "piid": 7}, # [-30, 100] step 1 | ||
# Alarm (siid=5) | ||
"buzzer": {"siid": 5, "piid": 1}, # bool | ||
# Light (siid=6) | ||
"led_light": {"siid": 6, "piid": 1}, # bool | ||
# Other (siid=7) | ||
"water_shortage_fault": {"siid": 7, "piid": 1}, # bool | ||
"tank_filed": {"siid": 7, "piid": 2}, # bool | ||
"overwet_protect": {"siid": 7, "piid": 3}, # bool | ||
} | ||
|
||
|
||
class AirHumidifierJsqsException(DeviceException): | ||
pass | ||
|
||
|
||
class OperationMode(enum.Enum): | ||
Low = 1 | ||
Mid = 2 | ||
High = 3 | ||
Auto = 4 | ||
|
||
|
||
class AirHumidifierJsqsStatus(DeviceStatus): | ||
"""Container for status reports from the air humidifier. | ||
Xiaomi Mi Smart Humidifer S (deerma.humidifier.[jsqs, jsq5]) respone (MIoT format) | ||
[ | ||
{'did': 'power', 'siid': 2, 'piid': 1, 'code': 0, 'value': True}, | ||
{'did': 'fault', 'siid': 2, 'piid': 2, 'code': 0, 'value': 0}, | ||
{'did': 'mode', 'siid': 2, 'piid': 5, 'code': 0, 'value': 1}, | ||
{'did': 'target_humidity', 'siid': 2, 'piid': 6, 'code': 0, 'value': 50}, | ||
{'did': 'relative_humidity', 'siid': 3, 'piid': 1, 'code': 0, 'value': 40}, | ||
{'did': 'temperature', 'siid': 3, 'piid': 7, 'code': 0, 'value': 22.7}, | ||
{'did': 'buzzer', 'siid': 5, 'piid': 1, 'code': 0, 'value': False}, | ||
{'did': 'led_light', 'siid': 6, 'piid': 1, 'code': 0, 'value': True}, | ||
{'did': 'water_shortage_fault', 'siid': 7, 'piid': 1, 'code': 0, 'value': False}, | ||
{'did': 'tank_filed', 'siid': 7, 'piid': 2, 'code': 0, 'value': False}, | ||
{'did': 'overwet_protect', 'siid': 7, 'piid': 3, 'code': 0, 'value': False} | ||
] | ||
""" | ||
|
||
def __init__(self, data: Dict[str, Any]) -> None: | ||
self.data = data | ||
|
||
# Air Humidifier | ||
|
||
@property | ||
def is_on(self) -> bool: | ||
"""Return True if device is on.""" | ||
return self.data["power"] | ||
|
||
@property | ||
def power(self) -> str: | ||
"""Return power state.""" | ||
return "on" if self.is_on else "off" | ||
|
||
@property | ||
def error(self) -> int: | ||
"""Return error state.""" | ||
return self.data["fault"] | ||
|
||
@property | ||
def mode(self) -> OperationMode: | ||
"""Return current operation mode.""" | ||
|
||
try: | ||
mode = OperationMode(self.data["mode"]) | ||
except ValueError as e: | ||
_LOGGER.exception("Cannot parse mode: %s", e) | ||
return OperationMode.Auto | ||
|
||
return mode | ||
|
||
@property | ||
def target_humidity(self) -> Optional[int]: | ||
"""Return target humidity.""" | ||
return self.data.get("target_humidity") | ||
|
||
# Environment | ||
|
||
@property | ||
def relative_humidity(self) -> Optional[int]: | ||
"""Return current humidity.""" | ||
return self.data.get("relative_humidity") | ||
|
||
@property | ||
def temperature(self) -> Optional[float]: | ||
"""Return current temperature, if available.""" | ||
return self.data.get("temperature") | ||
|
||
# Alarm | ||
|
||
@property | ||
def buzzer(self) -> Optional[bool]: | ||
"""Return True if buzzer is on.""" | ||
return self.data.get("buzzer") | ||
|
||
# Indicator Light | ||
|
||
@property | ||
def led_light(self) -> Optional[bool]: | ||
"""Return status of the LED.""" | ||
return self.data.get("led_light") | ||
|
||
# Other | ||
|
||
@property | ||
def tank_filed(self) -> Optional[bool]: | ||
"""Return the tank filed.""" | ||
return self.data.get("tank_filed") | ||
|
||
@property | ||
def water_shortage_fault(self) -> Optional[bool]: | ||
"""Return water shortage fault.""" | ||
return self.data.get("water_shortage_fault") | ||
|
||
@property | ||
def overwet_protect(self) -> Optional[bool]: | ||
"""Return True if overwet mode is active.""" | ||
return self.data.get("overwet_protect") | ||
|
||
|
||
class AirHumidifierJsqs(MiotDevice): | ||
"""Main class representing the air humidifier which uses MIoT protocol.""" | ||
|
||
_supported_models = ["deerma.humidifier.jsqs", "deerma.humidifier.jsq5"] | ||
|
||
mapping = _MAPPING | ||
|
||
@command( | ||
default_output=format_output( | ||
"", | ||
"Power: {result.power}\n" | ||
"Error: {result.error}\n" | ||
"Target Humidity: {result.target_humidity} %\n" | ||
"Relative Humidity: {result.relative_humidity} %\n" | ||
"Temperature: {result.temperature} °C\n" | ||
"Water tank detached: {result.tank_filed}\n" | ||
"Mode: {result.mode}\n" | ||
"LED light: {result.led_light}\n" | ||
"Buzzer: {result.buzzer}\n" | ||
"Overwet protection: {result.overwet_protect}\n", | ||
) | ||
) | ||
def status(self) -> AirHumidifierJsqsStatus: | ||
"""Retrieve properties.""" | ||
|
||
return AirHumidifierJsqsStatus( | ||
{ | ||
prop["did"]: prop["value"] if prop["code"] == 0 else None | ||
for prop in self.get_properties_for_mapping() | ||
} | ||
) | ||
|
||
@command(default_output=format_output("Powering on")) | ||
def on(self): | ||
"""Power on.""" | ||
return self.set_property("power", True) | ||
|
||
@command(default_output=format_output("Powering off")) | ||
def off(self): | ||
"""Power off.""" | ||
return self.set_property("power", False) | ||
|
||
@command( | ||
click.argument("humidity", type=int), | ||
default_output=format_output("Setting target humidity {humidity}%"), | ||
) | ||
def set_target_humidity(self, humidity: int): | ||
"""Set target humidity.""" | ||
if humidity < 40 or humidity > 80: | ||
raise AirHumidifierJsqsException( | ||
"Invalid target humidity: %s. Must be between 40 and 80" % humidity | ||
) | ||
return self.set_property("target_humidity", humidity) | ||
|
||
@command( | ||
click.argument("mode", type=EnumType(OperationMode)), | ||
default_output=format_output("Setting mode to '{mode.value}'"), | ||
) | ||
def set_mode(self, mode: OperationMode): | ||
"""Set working mode.""" | ||
return self.set_property("mode", mode.value) | ||
|
||
@command( | ||
click.argument("light", type=bool), | ||
default_output=format_output( | ||
lambda light: "Turning on LED light" if light else "Turning off LED light" | ||
), | ||
) | ||
def set_light(self, light: bool): | ||
"""Set led light.""" | ||
return self.set_property("led_light", light) | ||
|
||
@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) | ||
|
||
@command( | ||
click.argument("overwet", type=bool), | ||
default_output=format_output( | ||
lambda overwet: "Turning on overwet" if overwet else "Turning off overwet" | ||
), | ||
) | ||
def set_overwet_protect(self, overwet: bool): | ||
"""Set overwet mode on/off.""" | ||
return self.set_property("overwet_protect", overwet) |
Empty file.
Oops, something went wrong.