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 tinymu smart toiletlid #544

Merged
merged 18 commits into from
Sep 11, 2019
Merged
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Supported devices
- Xiaomi Smartmi Fresh Air System (:class:`miio.airfresh`)
- :doc:`Yeelight light bulbs <yeelight>` (:class:`miio.yeelight`) (only a very rudimentary support, use `python-yeelight <https://gitlab.com/stavros/python-yeelight/>`__ for a more complete support)
- Xiaomi Mi Air Dehumidifier (:class:`miio.airdehumidifier`)
- Xiaomi Tinymu Smart Toilet Cover (:class:`miio.toiletlid`)

*Feel free to create a pull request to add support for new devices as
well as additional features for supported devices.*
Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from miio.philips_moonlight import PhilipsMoonlight
from miio.powerstrip import PowerStrip
from miio.protocol import Message, Utils
from miio.toiletlid import Toiletlid
from miio.vacuum import Vacuum, VacuumException
from miio.vacuumcontainers import (VacuumStatus, ConsumableStatus, DNDStatus,
CleaningDetails, CleaningSummary, Timer, )
Expand Down
5 changes: 4 additions & 1 deletion miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from . import (Device, Vacuum, ChuangmiCamera, ChuangmiPlug, PowerStrip, AirPurifier, AirFresh,
Ceil, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight, ChuangmiIr,
AirHumidifier, WaterPurifier, WifiSpeaker, WifiRepeater,
Yeelight, Fan, Cooker, AirConditioningCompanion, AirQualityMonitor, AqaraCamera)
Yeelight, Fan, Cooker, AirConditioningCompanion, AirQualityMonitor, AqaraCamera,
Toiletlid)

from .airconditioningcompanion import (MODEL_ACPARTNER_V1, MODEL_ACPARTNER_V2, MODEL_ACPARTNER_V3, )
from .airqualitymonitor import (MODEL_AIRQUALITYMONITOR_V1, MODEL_AIRQUALITYMONITOR_B1,
Expand All @@ -23,6 +24,7 @@
from .fan import (MODEL_FAN_V2, MODEL_FAN_V3, MODEL_FAN_SA1, MODEL_FAN_ZA1, MODEL_FAN_ZA3,
MODEL_FAN_ZA4, MODEL_FAN_P5, )
from .powerstrip import (MODEL_POWER_STRIP_V1, MODEL_POWER_STRIP_V2, )
from .toiletlid import (MODEL_TOILETLID_V1, )

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -89,6 +91,7 @@
"zhimi-fan-za3": partial(Fan, model=MODEL_FAN_ZA3),
"zhimi-fan-za4": partial(Fan, model=MODEL_FAN_ZA4),
"dmaker-fan-p5": partial(Fan, model=MODEL_FAN_P5),
"tinymu-toiletlid-v1": partial(Toiletlid, model=MODEL_TOILETLID_V1),
"zhimi-airfresh-va2": AirFresh,
"zhimi-airmonitor-v1": partial(AirQualityMonitor, model=MODEL_AIRQUALITYMONITOR_V1),
"cgllc-airmonitor-b1": partial(AirQualityMonitor, model=MODEL_AIRQUALITYMONITOR_B1),
Expand Down
94 changes: 94 additions & 0 deletions miio/tests/test_toiletlid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from unittest import TestCase

import pytest

from miio.toiletlid import (
Toiletlid,
ToiletlidStatus,
AmbientLightColor,
MODEL_TOILETLID_V1,
)
from .dummies import DummyDevice

"""
Response instance
>> status

Work: False
State: 1
Ambient Light: Yellow
Filter remaining: 100%
Filter remaining time: 180
"""
rytilahti marked this conversation as resolved.
Show resolved Hide resolved


class DummyToiletlidV1(DummyDevice, Toiletlid):
def __init__(self, *args, **kwargs):
self.model = MODEL_TOILETLID_V1
self.state = {
"is_on": False,
"work_state": 1,
"ambient_light": "Yellow",
"filter_use_flux": "100",
"filter_use_time": "180",
}

self.return_values = {
"get_prop": self._get_state,
"nozzle_clean": lambda x: self._set_state("work_state", [97]),
"set_aled_v_of_uid": self.set_aled_v_of_uid,
"get_aled_v_of_uid": self.get_aled_v_of_uid,
}
super().__init__(args, kwargs)

def set_aled_v_of_uid(self, x):
uid, color = x
return self._set_state("ambient_light", [AmbientLightColor(color).name])

def get_aled_v_of_uid(self, uid):
color = self._get_state(["ambient_light"])
if not AmbientLightColor._member_map_.get(color[0]):
raise ValueError(color)
return AmbientLightColor._member_map_.get(color[0]).value


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


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

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

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

assert repr(self.state()) == repr(ToiletlidStatus(self.device.start_state))

assert self.is_on() is False
assert self.state().work_state == self.device.start_state["work_state"]
assert self.state().ambient_light == self.device.start_state["ambient_light"]
assert (
self.state().filter_use_percentage
== "%s%%" % self.device.start_state["filter_use_flux"]
)
assert (
self.state().filter_remaining_time
== self.device.start_state["filter_use_time"]
)

def test_set_ambient_light(self):
for value, enum in AmbientLightColor._member_map_.items():
self.device.set_ambient_light(enum)
assert self.device.status().ambient_light == value

def test_nozzle_clean(self):
self.device.nozzle_clean()
assert self.is_on() is True
self.device._reset_state()
143 changes: 143 additions & 0 deletions miio/toiletlid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import enum
import logging
from typing import Any, Dict

import click

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

_LOGGER = logging.getLogger(__name__)

MODEL_TOILETLID_V1 = "tinymu.toiletlid.v1"

AVAILABLE_PROPERTIES_COMMON = ["work_state", "filter_use_flux", "filter_use_time"]

AVAILABLE_PROPERTIES = {MODEL_TOILETLID_V1: AVAILABLE_PROPERTIES_COMMON}


class AmbientLightColor(enum.Enum):
White = "0"
Yellow = "1"
Powder = "2"
Green = "3"
Purple = "4"
Blue = "5"
Orange = "6"
Red = "7"


class ToiletlidStatus:
def __init__(self, data: Dict[str, Any]) -> None:
self.data = data

@property
def work_state(self) -> int:
rytilahti marked this conversation as resolved.
Show resolved Hide resolved
"""Device state code"""
return self.data["work_state"]

@property
def is_on(self) -> bool:
return self.work_state != 1

@property
def filter_use_percentage(self) -> str:
"""Filter percentage of remaining life"""
return "{}%".format(self.data["filter_use_flux"])

@property
def filter_remaining_time(self) -> int:
"""Filter remaining life days"""
return self.data["filter_use_time"]

@property
def ambient_light(self) -> str:
"""Ambient light color."""
return self.data["ambient_light"]

def __repr__(self) -> str:
return (
"<ToiletlidStatus work=%s, "
"state=%s, "
"ambient_light=%s, "
"filter_use_percentage=%s, "
"filter_remaining_time=%s>"
% (
self.is_on,
self.work_state,
self.ambient_light,
self.filter_use_percentage,
self.filter_remaining_time,
)
)


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

if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_TOILETLID_V1
rytilahti marked this conversation as resolved.
Show resolved Hide resolved

@command(
default_output=format_output(
"",
"Work: {result.is_on}\n"
"State: {result.work_state}\n"
"Ambient Light: {result.ambient_light}\n"
"Filter remaining: {result.filter_use_percentage}\n"
"Filter remaining time: {result.filter_remaining_time}\n",
)
)
def status(self) -> ToiletlidStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
values = self.send("get_prop", properties)
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,
)
color = self.get_ambient_light()
return ToiletlidStatus(dict(zip(properties, values), ambient_light=color))

@command(default_output=format_output("Nozzle clean"))
def nozzle_clean(self):
"""Nozzle clean."""
return self.send("nozzle_clean", ["on"])
rytilahti marked this conversation as resolved.
Show resolved Hide resolved

@command(
click.argument("color", type=EnumType(AmbientLightColor, False)),
default_output=format_output(
"Set the ambient light to {color} color the next time you start it."
),
)
def set_ambient_light(self, color: AmbientLightColor):
"""Set Ambient light color."""
return self.send("set_aled_v_of_uid", ["", color.value])

@command(default_output=format_output("Get the Ambient light color."))
def get_ambient_light(self) -> str:
"""Get Ambient light color."""
color = self.send("get_aled_v_of_uid", [""])
try:
return AmbientLightColor(color[0]).name
except ValueError:
_LOGGER.warning(
"Get ambient light response error, return unknown value: %s.", color[0]
)
return "Unknown"