Skip to content

Commit

Permalink
add support for smart pet water dispenser mmgg.pet_waterer.s1 (#1174)
Browse files Browse the repository at this point in the history
* add support for smart pet water dispenser mmgg.pet_waterer.s1

* state checks added

* more human-friendly properties

* toggleable methods converted to boolean switch + formating + type-hint fix

* set_power method renamed to is_on

* comments + type-hints fix

* reset_all_filters method added + manual sort for `status` output + comments + some status properties renamed to show return values e.g. days/minutes

* pet_water_dispenser moved to integrations/ + time properties now returns `timedelta` objects + code clean-up + status() return object comment + README update

* resolve imports after move

* #1174 (review)

* clean-up

* fix types, fix reset_all_filters(), comments
  • Loading branch information
ofen authored Nov 15, 2021
1 parent 452d436 commit a64c66a
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Supported devices
- Scishare coffee maker (scishare.coffee.s1102)
- Qingping Air Monitor Lite (cgllc.airm.cgdn1)
- Xiaomi Walkingpad A1 (ksmb.walkingpad.v3)
- Xiaomi Smart Pet Water Dispenser (mmgg.pet_waterer.s1, s4)


*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 @@ -40,6 +40,7 @@
from miio.heater import Heater
from miio.heater_miot import HeaterMiot
from miio.huizuo import Huizuo, HuizuoLampFan, HuizuoLampHeater, HuizuoLampScene
from miio.integrations.petwaterdispenser import PetWaterDispenser
from miio.integrations.vacuum.dreame.dreamevacuum_miot import DreameVacuumMiot
from miio.integrations.vacuum.mijia import G1Vacuum
from miio.integrations.vacuum.roborock import Vacuum, VacuumException
Expand Down
2 changes: 2 additions & 0 deletions miio/integrations/petwaterdispenser/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# flake8: noqa
from .device import PetWaterDispenser
146 changes: 146 additions & 0 deletions miio/integrations/petwaterdispenser/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import logging
from typing import Any, Dict, List

import click

from miio.click_common import EnumType, command, format_output
from miio.miot_device import MiotDevice

from .status import OperatingMode, PetWaterDispenserStatus

_LOGGER = logging.getLogger(__name__)

MODEL_MMGG_PET_WATERER_S1 = "mmgg.pet_waterer.s1"
MODEL_MMGG_PET_WATERER_S4 = "mmgg.pet_waterer.s4"

SUPPORTED_MODELS: List[str] = [MODEL_MMGG_PET_WATERER_S1, MODEL_MMGG_PET_WATERER_S4]

_MAPPING: Dict[str, Dict[str, int]] = {
# https://home.miot-spec.com/spec/mmgg.pet_waterer.s1
# https://home.miot-spec.com/spec/mmgg.pet_waterer.s4
"cotton_left_time": {"siid": 5, "piid": 1},
"reset_cotton_life": {"siid": 5, "aiid": 1},
"reset_clean_time": {"siid": 6, "aiid": 1},
"fault": {"siid": 2, "piid": 1},
"filter_left_time": {"siid": 3, "piid": 1},
"indicator_light": {"siid": 4, "piid": 1},
"lid_up_flag": {"siid": 7, "piid": 4}, # missing on mmgg.pet_waterer.s4
"location": {"siid": 9, "piid": 2},
"mode": {"siid": 2, "piid": 3},
"no_water_flag": {"siid": 7, "piid": 1},
"no_water_time": {"siid": 7, "piid": 2},
"on": {"siid": 2, "piid": 2},
"pump_block_flag": {"siid": 7, "piid": 3},
"remain_clean_time": {"siid": 6, "piid": 1},
"reset_filter_life": {"siid": 3, "aiid": 1},
"reset_device": {"siid": 8, "aiid": 1},
"timezone": {"siid": 9, "piid": 1},
}


class PetWaterDispenser(MiotDevice):
"""Main class representing the Pet Waterer / Pet Drinking Fountain / Smart Pet Water
Dispenser."""

mapping = _MAPPING
_supported_models = SUPPORTED_MODELS

@command(
default_output=format_output(
"",
"On: {result.is_on}\n"
"Mode: {result.mode}\n"
"LED on: {result.is_led_on}\n"
"Lid up: {result.is_lid_up}\n"
"No water: {result.is_no_water}\n"
"Time without water: {result.no_water_minutes}\n"
"Pump blocked: {result.is_pump_blocked}\n"
"Error detected: {result.is_error_detected}\n"
"Days before cleaning left: {result.before_cleaning_days}\n"
"Cotton filter live left: {result.cotton_left_days}\n"
"Sponge filter live left: {result.sponge_filter_left_days}\n"
"Location: {result.location}\n"
"Timezone: {result.timezone}\n",
)
)
def status(self) -> PetWaterDispenserStatus:
"""Retrieve properties."""
data = {
prop["did"]: prop["value"] if prop["code"] == 0 else None
for prop in self.get_properties_for_mapping()
}

_LOGGER.debug(data)

return PetWaterDispenserStatus(data)

@command(default_output=format_output("Turning device on"))
def on(self) -> List[Dict[str, Any]]:
"""Turn device on."""
return self.set_property("on", True)

@command(default_output=format_output("Turning device off"))
def off(self) -> List[Dict[str, Any]]:
"""Turn device off."""
return self.set_property("on", False)

@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning LED on" if led else "Turning LED off"
),
)
def set_led(self, led: bool) -> List[Dict[str, Any]]:
"""Toggle indicator light on/off."""
if led:
return self.set_property("indicator_light", True)
return self.set_property("indicator_light", False)

@command(
click.argument("mode", type=EnumType(OperatingMode)),
default_output=format_output('Changing mode to "{mode.name}"'),
)
def set_mode(self, mode: OperatingMode) -> List[Dict[str, Any]]:
"""Switch operation mode."""
return self.set_property("mode", mode.value)

@command(default_output=format_output("Resetting sponge filter"))
def reset_sponge_filter(self) -> Dict[str, Any]:
"""Reset sponge filter."""
return self.call_action("reset_filter_life")

@command(default_output=format_output("Resetting cotton filter"))
def reset_cotton_filter(self) -> Dict[str, Any]:
"""Reset cotton filter."""
return self.call_action("reset_cotton_life")

@command(default_output=format_output("Resetting all filters"))
def reset_all_filters(self) -> List[Dict[str, Any]]:
"""Reset all filters [cotton, sponge]."""
return [self.reset_cotton_filter(), self.reset_sponge_filter()]

@command(default_output=format_output("Resetting cleaning time"))
def reset_cleaning_time(self) -> Dict[str, Any]:
"""Reset cleaning time counter."""
return self.call_action("reset_clean_time")

@command(default_output=format_output("Resetting device"))
def reset(self) -> Dict[str, Any]:
"""Reset device."""
return self.call_action("reset_device")

@command(
click.argument("timezone", type=click.IntRange(-12, 12)),
default_output=format_output('Changing timezone to "{timezone}"'),
)
def set_timezone(self, timezone: int) -> List[Dict[str, Any]]:
"""Change timezone."""
return self.set_property("timezone", timezone)

@command(
click.argument("location", type=str),
default_output=format_output('Changing location to "{location}"'),
)
def set_location(self, location: str) -> List[Dict[str, Any]]:
"""Change location."""
return self.set_property("location", location)
101 changes: 101 additions & 0 deletions miio/integrations/petwaterdispenser/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import enum
from datetime import timedelta
from typing import Any, Dict

from miio.miot_device import DeviceStatus


class OperatingMode(enum.Enum):
Normal = 1
Smart = 2


class PetWaterDispenserStatus(DeviceStatus):
"""Container for status reports from Pet Water Dispenser."""

def __init__(self, data: Dict[str, Any]) -> None:
"""Response of Pet Water Dispenser (mmgg.pet_waterer.s1)
[
{'code': 0, 'did': 'cotton_left_time', 'piid': 1, 'siid': 5, 'value': 10},
{'code': 0, 'did': 'fault', 'piid': 1, 'siid': 2, 'value': 0},
{'code': 0, 'did': 'filter_left_time', 'piid': 1, 'siid': 3, 'value': 10},
{'code': 0, 'did': 'indicator_light', 'piid': 1, 'siid': 4, 'value': True},
{'code': 0, 'did': 'lid_up_flag', 'piid': 4, 'siid': 7, 'value': False},
{'code': 0, 'did': 'location', 'piid': 2, 'siid': 9, 'value': 'ru'},
{'code': 0, 'did': 'mode', 'piid': 3, 'siid': 2, 'value': 1},
{'code': 0, 'did': 'no_water_flag', 'piid': 1, 'siid': 7, 'value': True},
{'code': 0, 'did': 'no_water_time', 'piid': 2, 'siid': 7, 'value': 0},
{'code': 0, 'did': 'on', 'piid': 2, 'siid': 2, 'value': True},
{'code': 0, 'did': 'pump_block_flag', 'piid': 3, 'siid': 7, 'value': False},
{'code': 0, 'did': 'remain_clean_time', 'piid': 1, 'siid': 6, 'value': 4},
{'code': 0, 'did': 'timezone', 'piid': 1, 'siid': 9, 'value': 3}
]
"""
self.data = data

@property
def sponge_filter_left_days(self) -> timedelta:
"""Filter life time remaining in days."""
return timedelta(days=self.data["filter_left_time"])

@property
def is_on(self) -> bool:
"""True if device is on."""
return self.data["on"]

@property
def mode(self) -> OperatingMode:
"""OperatingMode."""
return OperatingMode(self.data["mode"])

@property
def is_led_on(self) -> bool:
"""True if enabled."""
return self.data["indicator_light"]

@property
def cotton_left_days(self) -> timedelta:
"""Cotton filter life time remaining in days."""
return timedelta(days=self.data["cotton_left_time"])

@property
def before_cleaning_days(self) -> timedelta:
"""Days before cleaning."""
return timedelta(days=self.data["remain_clean_time"])

@property
def is_no_water(self) -> bool:
"""True if there is no water left."""
if self.data["no_water_flag"]:
return False
return True

@property
def no_water_minutes(self) -> timedelta:
"""Minutes without water."""
return timedelta(minutes=self.data["no_water_time"])

@property
def is_pump_blocked(self) -> bool:
"""True if pump is blocked."""
return self.data["pump_block_flag"]

@property
def is_lid_up(self) -> bool:
"""True if lid is up."""
return self.data["lid_up_flag"]

@property
def timezone(self) -> int:
"""Timezone from -12 to +12."""
return self.data["timezone"]

@property
def location(self) -> str:
"""Device location string."""
return self.data["location"]

@property
def is_error_detected(self) -> bool:
"""True if fault detected."""
return self.data["fault"] > 0
Empty file.
37 changes: 37 additions & 0 deletions miio/integrations/petwaterdispenser/tests/test_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from datetime import timedelta

from ..status import OperatingMode, PetWaterDispenserStatus

data = {
"cotton_left_time": 10,
"fault": 0,
"filter_left_time": 10,
"indicator_light": True,
"lid_up_flag": False,
"location": "ru",
"mode": 1,
"no_water_flag": True,
"no_water_time": 0,
"on": True,
"pump_block_flag": False,
"remain_clean_time": 2,
"timezone": 3,
}


def test_status():
status = PetWaterDispenserStatus(data)

assert status.is_on is True
assert status.sponge_filter_left_days == timedelta(days=10)
assert status.mode == OperatingMode(1)
assert status.is_led_on is True
assert status.cotton_left_days == timedelta(days=10)
assert status.before_cleaning_days == timedelta(days=2)
assert status.is_no_water is False
assert status.no_water_minutes == timedelta(minutes=0)
assert status.is_pump_blocked is False
assert status.is_lid_up is False
assert status.timezone == 3
assert status.location == "ru"
assert status.is_error_detected is False

0 comments on commit a64c66a

Please sign in to comment.