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

Add support for deerma.humidifier.mjjsq #586

Merged
merged 13 commits into from
Dec 4, 2019
Merged
5 changes: 4 additions & 1 deletion miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
)
from miio.airdehumidifier import AirDehumidifier
from miio.airfresh import AirFresh
from miio.airhumidifier import AirHumidifier
from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1

from miio.airhumidifier_mjjsq import AirHumidifierMjjsq
from miio.airdehumidifier import AirDehumidifier
from miio.airpurifier import AirPurifier
from miio.airqualitymonitor import AirQualityMonitor
from miio.aqaracamera import AqaraCamera
Expand Down
Empty file modified miio/airdehumidifier.py
100755 → 100644
Empty file.
236 changes: 236 additions & 0 deletions miio/airhumidifier_mjjsq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import enum
import logging
from collections import defaultdict
from typing import Any, Dict

import click

from .click_common import EnumType, command, format_output
from .device import Device, DeviceException

_LOGGER = logging.getLogger(__name__)

MODEL_HUMIDIFIER_MJJSQ = "deerma.humidifier.mjjsq"

AVAILABLE_PROPERTIES = {
MODEL_HUMIDIFIER_MJJSQ: [
"OnOff_State",
"TemperatureValue",
"Humidity_Value",
"HumiSet_Value",
"Humidifier_Gear",
"Led_State",
"TipSound_State",
"waterstatus",
"watertankstatus",
]
}


class AirHumidifierMjjsqException(DeviceException):
pass


class OperationModeMjjsq(enum.Enum):
Low = 1
Medium = 2
High = 3
Humidity = 4


class AirHumidifierMjjsqStatus:
syssi marked this conversation as resolved.
Show resolved Hide resolved
"""Container for status reports from the air humidifier mjjsq."""

def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Air Humidifier (deerma.humidifier.mjjsq):

{'Humidifier_Gear': 4, 'Humidity_Value': 44, 'HumiSet_Value': 54,
'Led_State': 1, 'OnOff_State': 0, 'TemperatureValue': 21,
'TipSound_State': 1, 'waterstatus': 1, 'watertankstatus': 1}
"""

self.data = data

@property
def power(self) -> str:
"""Power state."""
return "on" if self.data["OnOff_State"] == 1 else "off"
Copy link
Owner

Choose a reason for hiding this comment

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

Is there a need to have both power and is_on? I'd prefer simply a single boolean variant of one of these.

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 would like to stick on this because of consistency and a nice status output of: "Power: on"

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, your call.


@property
def is_on(self) -> bool:
"""True if device is turned on."""
return self.power == "on"

@property
def mode(self) -> OperationModeMjjsq:
"""Operation mode. Can be either low, medium, high or humidity."""
return OperationModeMjjsq(self.data["Humidifier_Gear"])

@property
def temperature(self) -> int:
"""Current temperature."""
syssi marked this conversation as resolved.
Show resolved Hide resolved
return self.data["TemperatureValue"]

@property
def humidity(self) -> int:
"""Current humidity."""
syssi marked this conversation as resolved.
Show resolved Hide resolved
return self.data["Humidity_Value"]

@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["TipSound_State"] == 1

@property
def led(self) -> bool:
"""True if LED is turned on."""
return self.data["Led_State"] == 1

@property
def target_humidity(self) -> int:
"""Target humiditiy."""
syssi marked this conversation as resolved.
Show resolved Hide resolved
return self.data["HumiSet_Value"]

@property
def no_water(self) -> bool:
"""True if the water tank is empty."""
return self.data["waterstatus"] == 0

@property
def water_tank_detached(self) -> bool:
"""True if the water tank is detached."""
return self.data["watertankstatus"] == 0

def __repr__(self) -> str:
s = (
"<AirHumidiferStatusMjjsq power=%s, "
"mode=%s, "
"temperature=%s, "
"humidity=%s%%, "
"led_brightness=%s, "
"buzzer=%s, "
"target_humidity=%s%%, "
"no_water=%s, "
"water_tank_detached=%s>"
% (
self.power,
self.mode,
self.temperature,
self.humidity,
self.led,
self.buzzer,
self.target_humidity,
self.no_water,
self.water_tank_detached,
)
)
return s

def __json__(self):
return self.data


class AirHumidifierMjjsq(Device):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_HUMIDIFIER_MJJSQ,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)

if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_HUMIDIFIER_MJJSQ

@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Mode: {result.mode}\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"LED: {result.led}\n"
"Buzzer: {result.buzzer}\n"
"Target humidity: {result.target_humidity} %\n"
"No water: {result.no_water}\n"
"Water tank detached: {result.water_tank_detached}\n",
)
)
def status(self) -> AirHumidifierMjjsqStatus:
"""Retrieve properties."""

properties = AVAILABLE_PROPERTIES[self.model]
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:1]))
_props[:] = _props[1:]

properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)

return AirHumidifierMjjsqStatus(
defaultdict(lambda: None, zip(properties, values))
)

@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("Set_OnOff", [1])

@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("Set_OnOff", [0])

@command(
click.argument("mode", type=EnumType(OperationModeMjjsq, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationModeMjjsq):
"""Set mode."""
return self.send("Set_HumidifierGears", [mode.value])

@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
return self.send("SetLedState", [int(led)])

@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.send("SetTipSound_Status", [int(buzzer)])

@command(
click.argument("humidity", type=int),
default_output=format_output("Setting target humidity to {humidity}"),
)
def set_target_humidity(self, humidity: int):
"""Set the target humidity."""
if humidity < 0 or humidity > 99:
raise AirHumidifierMjjsqException("Invalid target humidity: %s" % humidity)

return self.send("Set_HumiValue", [humidity])
142 changes: 142 additions & 0 deletions miio/tests/test_airhumidifier_mjjsq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from unittest import TestCase

import pytest

from miio import AirHumidifierMjjsq
from miio.airhumidifier_mjjsq import (
MODEL_HUMIDIFIER_MJJSQ,
AirHumidifierMjjsqException,
AirHumidifierMjjsqStatus,
OperationModeMjjsq,
)
from .dummies import DummyDevice


class DummyAirHumidifierMjjsq(DummyDevice, AirHumidifierMjjsq):
def __init__(self, *args, **kwargs):
self.model = MODEL_HUMIDIFIER_MJJSQ
self.state = {
"Humidifier_Gear": 1,
"Humidity_Value": 44,
"HumiSet_Value": 11,
"Led_State": 0,
"OnOff_State": 1,
"TemperatureValue": 21,
"TipSound_State": 0,
"waterstatus": 1,
"watertankstatus": 1,
}
self.return_values = {
"get_prop": self._get_state,
"Set_OnOff": lambda x: self._set_state("OnOff_State", x),
"Set_HumidifierGears": lambda x: self._set_state("Humidifier_Gear", x),
"SetLedState": lambda x: self._set_state("Led_State", x),
"SetTipSound_Status": lambda x: self._set_state("TipSound_State", x),
"Set_HumiValue": lambda x: self._set_state("HumiSet_Value", x),
}
super().__init__(args, kwargs)


@pytest.fixture(scope="class")
def airhumidifiermjjsq(request):
request.cls.device = DummyAirHumidifierMjjsq()
# TODO add ability to test on a real device


@pytest.mark.usefixtures("airhumidifiermjjsq")
class TestAirHumidifierMjjsq(TestCase):
def is_on(self):
return self.device.status().is_on

def state(self):
return self.device.status()

def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False

self.device.on()
assert self.is_on() is True

def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True

self.device.off()
assert self.is_on() is False

def test_status(self):
self.device._reset_state()

assert repr(self.state()) == repr(
AirHumidifierMjjsqStatus(self.device.start_state)
)
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["TemperatureValue"]
assert self.state().humidity == self.device.start_state["Humidity_Value"]
assert self.state().mode == OperationModeMjjsq(
self.device.start_state["Humidifier_Gear"]
)
assert self.state().led is (self.device.start_state["Led_State"] == 1)
assert self.state().buzzer is (self.device.start_state["TipSound_State"] == 1)
assert self.state().target_humidity == self.device.start_state["HumiSet_Value"]
assert self.state().no_water is (self.device.start_state["waterstatus"] == 0)
assert self.state().water_tank_detached is (
self.device.start_state["watertankstatus"] == 0
)

def test_set_mode(self):
def mode():
return self.device.status().mode

self.device.set_mode(OperationModeMjjsq.Low)
assert mode() == OperationModeMjjsq.Low

self.device.set_mode(OperationModeMjjsq.Medium)
assert mode() == OperationModeMjjsq.Medium

self.device.set_mode(OperationModeMjjsq.High)
assert mode() == OperationModeMjjsq.High

self.device.set_mode(OperationModeMjjsq.Humidity)
assert mode() == OperationModeMjjsq.Humidity

def test_set_led(self):
def led():
return self.device.status().led

self.device.set_led(True)
assert led() is True

self.device.set_led(False)
assert led() is False

def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer

self.device.set_buzzer(True)
assert buzzer() is True

self.device.set_buzzer(False)
assert buzzer() is False

def test_set_target_humidity(self):
def target_humidity():
return self.device.status().target_humidity

self.device.set_target_humidity(0)
assert target_humidity() == 0
self.device.set_target_humidity(50)
assert target_humidity() == 50
self.device.set_target_humidity(99)
assert target_humidity() == 99

with pytest.raises(AirHumidifierMjjsqException):
self.device.set_target_humidity(-1)

with pytest.raises(AirHumidifierMjjsqException):
self.device.set_target_humidity(100)

with pytest.raises(AirHumidifierMjjsqException):
self.device.set_target_humidity(101)