Skip to content

Commit

Permalink
Add support for deerma.humidifier.jsq{s,5} (#1193)
Browse files Browse the repository at this point in the history
* 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
supar and syssi authored Jan 22, 2022
1 parent 17e713a commit 7daa6e1
Show file tree
Hide file tree
Showing 8 changed files with 378 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Supported devices
- Qingping Air Monitor Lite (cgllc.airm.cgdn1)
- Xiaomi Walkingpad A1 (ksmb.walkingpad.v3)
- Xiaomi Smart Pet Water Dispenser (mmgg.pet_waterer.s1, s4)
- Xiaomi Mi Smart Humidifer S (jsqs, jsq5)


*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
Expand Up @@ -45,6 +45,7 @@
from miio.integrations.fan.dmaker import Fan1C, FanMiot, FanP5, FanP9, FanP10, FanP11
from miio.integrations.fan.leshow import FanLeshow
from miio.integrations.fan.zhimi import Fan, FanZA5
from miio.integrations.humidifier.deerma import AirHumidifierJsqs
from miio.integrations.light.philips import (
Ceil,
PhilipsBulb,
Expand Down
2 changes: 2 additions & 0 deletions miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
AirFreshT2017,
AirHumidifier,
AirHumidifierJsq,
AirHumidifierJsqs,
AirHumidifierMjjsq,
AirPurifier,
AirPurifierMiot,
Expand Down Expand Up @@ -137,6 +138,7 @@
AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_MJJSQ
),
"deerma-humidifier-jsq1": partial(AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_JSQ1),
"deerma-humidifier-jsqs": AirHumidifierJsqs,
"yunmi-waterpuri-v2": WaterPurifier,
"yunmi.waterpuri.lx9": WaterPurifierYunmi,
"yunmi.waterpuri.lx11": WaterPurifierYunmi,
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions miio/integrations/humidifier/deerma/__init__.py
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 miio/integrations/humidifier/deerma/airhumidifier_jsqs.py
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.
Loading

0 comments on commit 7daa6e1

Please sign in to comment.