Skip to content

Commit 7daa6e1

Browse files
suparsyssi
andauthored
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 <basti@linkt.de>
1 parent 17e713a commit 7daa6e1

8 files changed

Lines changed: 378 additions & 0 deletions

File tree

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Supported devices
151151
- Qingping Air Monitor Lite (cgllc.airm.cgdn1)
152152
- Xiaomi Walkingpad A1 (ksmb.walkingpad.v3)
153153
- Xiaomi Smart Pet Water Dispenser (mmgg.pet_waterer.s1, s4)
154+
- Xiaomi Mi Smart Humidifer S (jsqs, jsq5)
154155

155156

156157
*Feel free to create a pull request to add support for new devices as

miio/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from miio.integrations.fan.dmaker import Fan1C, FanMiot, FanP5, FanP9, FanP10, FanP11
4646
from miio.integrations.fan.leshow import FanLeshow
4747
from miio.integrations.fan.zhimi import Fan, FanZA5
48+
from miio.integrations.humidifier.deerma import AirHumidifierJsqs
4849
from miio.integrations.light.philips import (
4950
Ceil,
5051
PhilipsBulb,

miio/discovery.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
AirFreshT2017,
2222
AirHumidifier,
2323
AirHumidifierJsq,
24+
AirHumidifierJsqs,
2425
AirHumidifierMjjsq,
2526
AirPurifier,
2627
AirPurifierMiot,
@@ -137,6 +138,7 @@
137138
AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_MJJSQ
138139
),
139140
"deerma-humidifier-jsq1": partial(AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_JSQ1),
141+
"deerma-humidifier-jsqs": AirHumidifierJsqs,
140142
"yunmi-waterpuri-v2": WaterPurifier,
141143
"yunmi.waterpuri.lx9": WaterPurifierYunmi,
142144
"yunmi.waterpuri.lx11": WaterPurifierYunmi,

miio/integrations/humidifier/__init__.py

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# flake8: noqa
2+
from .airhumidifier_jsqs import AirHumidifierJsqs
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import enum
2+
import logging
3+
from typing import Any, Dict, Optional
4+
5+
import click
6+
7+
from miio.click_common import EnumType, command, format_output
8+
from miio.exceptions import DeviceException
9+
from miio.miot_device import DeviceStatus, MiotDevice
10+
11+
_LOGGER = logging.getLogger(__name__)
12+
_MAPPING = {
13+
# Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:humidifier:0000A00E:deerma-jsqs:2
14+
# Air Humidifier (siid=2)
15+
"power": {"siid": 2, "piid": 1}, # bool
16+
"fault": {"siid": 2, "piid": 2}, # 0
17+
"mode": {"siid": 2, "piid": 5}, # 1 - lvl1, 2 - lvl2, 3 - lvl3, 4 - auto
18+
"target_humidity": {"siid": 2, "piid": 6}, # [40, 80] step 1
19+
# Environment (siid=3)
20+
"relative_humidity": {"siid": 3, "piid": 1}, # [0, 100] step 1
21+
"temperature": {"siid": 3, "piid": 7}, # [-30, 100] step 1
22+
# Alarm (siid=5)
23+
"buzzer": {"siid": 5, "piid": 1}, # bool
24+
# Light (siid=6)
25+
"led_light": {"siid": 6, "piid": 1}, # bool
26+
# Other (siid=7)
27+
"water_shortage_fault": {"siid": 7, "piid": 1}, # bool
28+
"tank_filed": {"siid": 7, "piid": 2}, # bool
29+
"overwet_protect": {"siid": 7, "piid": 3}, # bool
30+
}
31+
32+
33+
class AirHumidifierJsqsException(DeviceException):
34+
pass
35+
36+
37+
class OperationMode(enum.Enum):
38+
Low = 1
39+
Mid = 2
40+
High = 3
41+
Auto = 4
42+
43+
44+
class AirHumidifierJsqsStatus(DeviceStatus):
45+
"""Container for status reports from the air humidifier.
46+
47+
Xiaomi Mi Smart Humidifer S (deerma.humidifier.[jsqs, jsq5]) respone (MIoT format)
48+
49+
[
50+
{'did': 'power', 'siid': 2, 'piid': 1, 'code': 0, 'value': True},
51+
{'did': 'fault', 'siid': 2, 'piid': 2, 'code': 0, 'value': 0},
52+
{'did': 'mode', 'siid': 2, 'piid': 5, 'code': 0, 'value': 1},
53+
{'did': 'target_humidity', 'siid': 2, 'piid': 6, 'code': 0, 'value': 50},
54+
{'did': 'relative_humidity', 'siid': 3, 'piid': 1, 'code': 0, 'value': 40},
55+
{'did': 'temperature', 'siid': 3, 'piid': 7, 'code': 0, 'value': 22.7},
56+
{'did': 'buzzer', 'siid': 5, 'piid': 1, 'code': 0, 'value': False},
57+
{'did': 'led_light', 'siid': 6, 'piid': 1, 'code': 0, 'value': True},
58+
{'did': 'water_shortage_fault', 'siid': 7, 'piid': 1, 'code': 0, 'value': False},
59+
{'did': 'tank_filed', 'siid': 7, 'piid': 2, 'code': 0, 'value': False},
60+
{'did': 'overwet_protect', 'siid': 7, 'piid': 3, 'code': 0, 'value': False}
61+
]
62+
"""
63+
64+
def __init__(self, data: Dict[str, Any]) -> None:
65+
self.data = data
66+
67+
# Air Humidifier
68+
69+
@property
70+
def is_on(self) -> bool:
71+
"""Return True if device is on."""
72+
return self.data["power"]
73+
74+
@property
75+
def power(self) -> str:
76+
"""Return power state."""
77+
return "on" if self.is_on else "off"
78+
79+
@property
80+
def error(self) -> int:
81+
"""Return error state."""
82+
return self.data["fault"]
83+
84+
@property
85+
def mode(self) -> OperationMode:
86+
"""Return current operation mode."""
87+
88+
try:
89+
mode = OperationMode(self.data["mode"])
90+
except ValueError as e:
91+
_LOGGER.exception("Cannot parse mode: %s", e)
92+
return OperationMode.Auto
93+
94+
return mode
95+
96+
@property
97+
def target_humidity(self) -> Optional[int]:
98+
"""Return target humidity."""
99+
return self.data.get("target_humidity")
100+
101+
# Environment
102+
103+
@property
104+
def relative_humidity(self) -> Optional[int]:
105+
"""Return current humidity."""
106+
return self.data.get("relative_humidity")
107+
108+
@property
109+
def temperature(self) -> Optional[float]:
110+
"""Return current temperature, if available."""
111+
return self.data.get("temperature")
112+
113+
# Alarm
114+
115+
@property
116+
def buzzer(self) -> Optional[bool]:
117+
"""Return True if buzzer is on."""
118+
return self.data.get("buzzer")
119+
120+
# Indicator Light
121+
122+
@property
123+
def led_light(self) -> Optional[bool]:
124+
"""Return status of the LED."""
125+
return self.data.get("led_light")
126+
127+
# Other
128+
129+
@property
130+
def tank_filed(self) -> Optional[bool]:
131+
"""Return the tank filed."""
132+
return self.data.get("tank_filed")
133+
134+
@property
135+
def water_shortage_fault(self) -> Optional[bool]:
136+
"""Return water shortage fault."""
137+
return self.data.get("water_shortage_fault")
138+
139+
@property
140+
def overwet_protect(self) -> Optional[bool]:
141+
"""Return True if overwet mode is active."""
142+
return self.data.get("overwet_protect")
143+
144+
145+
class AirHumidifierJsqs(MiotDevice):
146+
"""Main class representing the air humidifier which uses MIoT protocol."""
147+
148+
_supported_models = ["deerma.humidifier.jsqs", "deerma.humidifier.jsq5"]
149+
150+
mapping = _MAPPING
151+
152+
@command(
153+
default_output=format_output(
154+
"",
155+
"Power: {result.power}\n"
156+
"Error: {result.error}\n"
157+
"Target Humidity: {result.target_humidity} %\n"
158+
"Relative Humidity: {result.relative_humidity} %\n"
159+
"Temperature: {result.temperature} °C\n"
160+
"Water tank detached: {result.tank_filed}\n"
161+
"Mode: {result.mode}\n"
162+
"LED light: {result.led_light}\n"
163+
"Buzzer: {result.buzzer}\n"
164+
"Overwet protection: {result.overwet_protect}\n",
165+
)
166+
)
167+
def status(self) -> AirHumidifierJsqsStatus:
168+
"""Retrieve properties."""
169+
170+
return AirHumidifierJsqsStatus(
171+
{
172+
prop["did"]: prop["value"] if prop["code"] == 0 else None
173+
for prop in self.get_properties_for_mapping()
174+
}
175+
)
176+
177+
@command(default_output=format_output("Powering on"))
178+
def on(self):
179+
"""Power on."""
180+
return self.set_property("power", True)
181+
182+
@command(default_output=format_output("Powering off"))
183+
def off(self):
184+
"""Power off."""
185+
return self.set_property("power", False)
186+
187+
@command(
188+
click.argument("humidity", type=int),
189+
default_output=format_output("Setting target humidity {humidity}%"),
190+
)
191+
def set_target_humidity(self, humidity: int):
192+
"""Set target humidity."""
193+
if humidity < 40 or humidity > 80:
194+
raise AirHumidifierJsqsException(
195+
"Invalid target humidity: %s. Must be between 40 and 80" % humidity
196+
)
197+
return self.set_property("target_humidity", humidity)
198+
199+
@command(
200+
click.argument("mode", type=EnumType(OperationMode)),
201+
default_output=format_output("Setting mode to '{mode.value}'"),
202+
)
203+
def set_mode(self, mode: OperationMode):
204+
"""Set working mode."""
205+
return self.set_property("mode", mode.value)
206+
207+
@command(
208+
click.argument("light", type=bool),
209+
default_output=format_output(
210+
lambda light: "Turning on LED light" if light else "Turning off LED light"
211+
),
212+
)
213+
def set_light(self, light: bool):
214+
"""Set led light."""
215+
return self.set_property("led_light", light)
216+
217+
@command(
218+
click.argument("buzzer", type=bool),
219+
default_output=format_output(
220+
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
221+
),
222+
)
223+
def set_buzzer(self, buzzer: bool):
224+
"""Set buzzer on/off."""
225+
return self.set_property("buzzer", buzzer)
226+
227+
@command(
228+
click.argument("overwet", type=bool),
229+
default_output=format_output(
230+
lambda overwet: "Turning on overwet" if overwet else "Turning off overwet"
231+
),
232+
)
233+
def set_overwet_protect(self, overwet: bool):
234+
"""Set overwet mode on/off."""
235+
return self.set_property("overwet_protect", overwet)

miio/integrations/humidifier/deerma/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)