Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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