-
-
Notifications
You must be signed in to change notification settings - Fork 572
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 inital support for mmgg.feeder.petfeeder #1210
Changes from all commits
56cd67d
4672972
d3f538d
ddb6281
520a57a
d14b8ed
29965e7
8a3b2a5
9b92a09
9495768
cebf8e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# flake8: noqa | ||
from .device import PetFoodDispenser |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,111 @@ | ||||||
import logging | ||||||
from typing import Any, Dict, List | ||||||
|
||||||
from collections import defaultdict | ||||||
import click | ||||||
|
||||||
from miio.click_common import EnumType, command, format_output | ||||||
from miio import Device | ||||||
|
||||||
|
||||||
from .status import PetFoodDispenserStatus | ||||||
|
||||||
_LOGGER = logging.getLogger(__name__) | ||||||
|
||||||
MODEL_MMGG_FEEDER_PETFEEDER = "mmgg.feeder.petfeeder" | ||||||
|
||||||
SUPPORTED_MODELS: List[str] = [MODEL_MMGG_FEEDER_PETFEEDER] | ||||||
|
||||||
AVAILABLE_PROPERTIES: Dict[str, List[str]] = { | ||||||
MODEL_MMGG_FEEDER_PETFEEDER: [ | ||||||
"food_status", | ||||||
"feed_plan", | ||||||
"door_status", | ||||||
"feed_today", | ||||||
"clean_days", | ||||||
"power_status", | ||||||
"dryer_days", | ||||||
"food_portion", | ||||||
"wifi_led", | ||||||
"key_lock", | ||||||
"country_code", | ||||||
], | ||||||
} | ||||||
|
||||||
class PetFoodDispenser(Device): | ||||||
"""Main class representing the Pet Feeder / Smart Pet Food Dispenser. """ | ||||||
|
||||||
_supported_models = SUPPORTED_MODELS | ||||||
|
||||||
@command( | ||||||
default_output=format_output( | ||||||
"", | ||||||
"Power source: {result.power_status}\n" | ||||||
"Food level: {result.food_status}\n" | ||||||
"Automatic feeding: {result.feed_plan}\n" | ||||||
"Food bin lid: {result.door_status}\n" | ||||||
"Dispense button lock: {result.key_lock}\n" | ||||||
"Days until clean: {result.clean_days}\n" | ||||||
"Desiccant life: {result.dryer_days}\n" | ||||||
"WiFi LED: {result.wifi_led}\n", | ||||||
) | ||||||
) | ||||||
def status(self) -> PetFoodDispenserStatus: | ||||||
"""Retrieve properties.""" | ||||||
properties = AVAILABLE_PROPERTIES.get( | ||||||
self.model, AVAILABLE_PROPERTIES[MODEL_MMGG_FEEDER_PETFEEDER] | ||||||
) | ||||||
values = self.send('getprops') | ||||||
return PetFoodDispenserStatus(defaultdict(lambda: None, zip(properties, values))) | ||||||
|
||||||
@command( | ||||||
click.argument("amount", type=int), | ||||||
default_output=format_output("Dispensing {amount} units)"), | ||||||
) | ||||||
def dispense_food(self, amount: int): | ||||||
"""Dispense food. | ||||||
:param amount: in units (1 unit ~= 5g) | ||||||
""" | ||||||
return self.send("outfood", [amount]) | ||||||
|
||||||
@command(default_output=format_output("Resetting clean time")) | ||||||
def reset_clean_time(self) -> bool: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure this returns a bool? And same for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, it returns a list with a string of "ok". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's common on commands, coming likely from the SDK. You could either check for the "ok" in the payload or simply return it as it is, there is no real consistency in the library at the moment on that. |
||||||
"""Reset clean time.""" | ||||||
return self.send("resetclean") | ||||||
|
||||||
@command(default_output=format_output("Resetting dryer time")) | ||||||
def reset_dryer_time(self) -> bool: | ||||||
"""Reset dryer time.""" | ||||||
return self.send("resetdryer") | ||||||
|
||||||
@command( | ||||||
click.argument("state", type=int), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Use bool for boolean values across the board. This should work directly for the cli input values so no need for explicit conversions either. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used bool initially but seemingly then gets sent as literal "true" or "false" (not 1 or 0) which the device does not understand. miot spec for other mmgg products have these defined as ints rather than bools. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, then casting is needed; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤦 |
||||||
default_output=format_output( | ||||||
lambda state: "Turning on WiFi LED" if state else "Turning off WiFi LED" | ||||||
), | ||||||
) | ||||||
def set_wifi_led(self, state: int): | ||||||
rytilahti marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
"""Enable / Disable the wifi status led.""" | ||||||
return self.send("wifiledon", [state]) | ||||||
|
||||||
@command( | ||||||
click.argument("state", type=int), | ||||||
default_output=format_output( | ||||||
lambda state: "Enabling key lock for dispense button" if state else "Disabling key lock for dispense button" | ||||||
), | ||||||
) | ||||||
def set_key_lock(self, state: int): | ||||||
"""Enable / Disable the key lock for the manual dispense button.""" | ||||||
return self.send("keylock", [state ^ 1]) | ||||||
|
||||||
@command( | ||||||
click.argument("state", type=int), | ||||||
default_output=format_output( | ||||||
lambda state: "Enabling automatic feeding schedule" if state else "Disabling automatic feeding schedule" | ||||||
), | ||||||
) | ||||||
def set_feed_state(self, state: int): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Naming stuff is the hardest part of all this, honestly 😆 In mihome the toggle is "automatic feeding schedule". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Btw, you can run the linting tests locally by executing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I must admit I didn't with this commit, I will in future 👼 |
||||||
"""Enable / Disable the automatic feeding schedule.""" | ||||||
return self.send("stopfeed", [state]) | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import enum | ||
from typing import Any, Dict | ||
|
||
from miio import DeviceStatus | ||
|
||
class FoodStatus(enum.Enum): | ||
Normal = 0 | ||
Low = 1 | ||
Empty = 2 | ||
|
||
class PowerState(enum.Enum): | ||
Mains = 0 | ||
Battery = 1 | ||
|
||
class PetFoodDispenserStatus(DeviceStatus): | ||
"""Container for status reports from the Pet Feeder / Smart Pet Food Dispenser.""" | ||
|
||
def __init__(self, data: Dict[str, Any]) -> None: | ||
""" | ||
Response from pet feeder (mmgg.feeder.petfeeder): | ||
|
||
{'food_status': 0, 'feed_plan': 1, 'door_status': 0, | ||
'feed_today': 0, 'clean_time': 7, 'power_status': 0, | ||
'dryer_days': 6, 'food_portion': 0, 'wifi_led': 1, | ||
'key_lock': 1, 'country_code': 255,} | ||
""" | ||
self.data = data | ||
Wh1terat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@property | ||
def food_status(self) -> FoodStatus: | ||
"""Current food status / level.""" | ||
return FoodStatus(self.data["food_status"]) | ||
|
||
@property | ||
def feed_plan(self) -> bool: | ||
"""Automatic feeding status.""" | ||
return bool(self.data["feed_plan"]) | ||
|
||
@property | ||
def door_status(self) -> bool: | ||
"""Food bin door status.""" | ||
return bool(self.data["door_status"]) | ||
|
||
@property | ||
def clean_days(self) -> int: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perfect, was just about to ask about this. Can't quite think how to phrase "clean_days". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have a good term for that either, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that's what I was trying to avoid, even though it's very pythonic to make it as long as is needed to convey it's use. |
||
"""Number of days until the unit requires cleaning.""" | ||
return self.data['clean_days'] | ||
|
||
@property | ||
def power_status(self) -> str: | ||
"""Power status.""" | ||
return PowerState(self.data["power_status"]) | ||
|
||
@property | ||
def dryer_days(self) -> int: | ||
"""Number of days until the desiccant disc requires replacing.""" | ||
return self.data['dryer_days'] | ||
|
||
@property | ||
def wifi_led(self) -> bool: | ||
"""WiFi LED Status.""" | ||
return bool(self.data["wifi_led"]) | ||
|
||
@property | ||
def key_lock(self) -> bool: | ||
"""Key lock status for manual dispense button.""" | ||
return bool(self.data["key_lock"]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from ..status import FoodStatus, PowerState, PetFoodDispenserStatus | ||
|
||
data = { | ||
"food_status": 0, | ||
"feed_plan": 1, | ||
"door_status": 0, | ||
"feed_today": 0, | ||
"clean_time": 7, | ||
"power_status": 0, | ||
"dryer_days": 6, | ||
"food_portion": 0, | ||
"wifi_led": 1, | ||
"key_lock": 1, | ||
"country_code": 255, | ||
} | ||
|
||
def test_status(): | ||
status = PetFoodDispenserStatus(data) | ||
|
||
assert status.food_status == FoodStatus(0) | ||
assert status.feed_plan is True | ||
assert status.door_status is False | ||
assert status.feed_today == 0 | ||
assert status.clean_time == 7 | ||
assert status.power_status == PowerState(0) | ||
assert status.dryer_days == 6 | ||
assert status.food_portion == 0 | ||
assert status.wifi_led is True | ||
assert status.key_lock is False | ||
assert status.country_code == 255 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would make sense to avoid using the property names from the protocol itself, but use more developer friendly names (similar to use for these printouts).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah I was going to ask about that.
Some names I've taken from miot spec for another feeder by mmgg (one that's actually compliant... 😠 ), but obviously some things don't translate overly well and wasn't sure on the preferred approach.
So you're suggesting (for example) "door_status" > "food_bin_lid" or something along those lines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. It makes sense to separate the property names from what is being internally used, as most of the times they are not clear in their meaning.
bin_lid_open
, maybe?