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 inital support for mmgg.feeder.petfeeder #1210

Closed
wants to merge 11 commits into from
Closed

Add inital support for mmgg.feeder.petfeeder #1210

wants to merge 11 commits into from

Conversation

Wh1terat
Copy link

@Wh1terat Wh1terat commented Dec 6, 2021

I've tried to follow the style as best I can see.
Was 50/50 on calling the integration "PetfoodDispenser" (keeps with PetWaterDispenser) or simply calling it "PetFeeder".

This device does not follow it's own (somewhat limited) miot spec (annoying since the Manufacturer does make other compliant devices!) so this is all MiiO.

There's a number of features unavailable vs the mi home app:

  • Fault status (seemingly only available via push to mihome)
  • Feed total (seemingly stored in the plugin within mihome)

Added most functions:

Usage: miiocli petfooddispenser [OPTIONS] COMMAND [ARGS]...

Options:
  --ip TEXT     [required]
  --token TEXT  [required]
  --model TEXT
  --help        Show this message and exit.

Commands:
  dispense_food     Dispense food.
  info              Get (and cache) miIO protocol information from the...
  raw_command       Send a raw command to the device.
  reset_clean_time  Reset clean time.
  reset_dryer_time  Reset dryer time.
  set_feed_state    Enable / Disable the automatic feeding schedule.
  set_key_lock      Enable / Disable the key lock for the manual dispense...
  set_wifi_led      Enable / Disable the wifi status led.
  status            Retrieve properties.
  test_properties   Helper to test device properties.

Usage: miiocli petfooddispenser dispense_food [OPTIONS] AMOUNT

  Dispense food. :param amount: in units (1 unit ~= 5g)

Usage: miiocli petfooddispenser reset_clean_time [OPTIONS]

  Reset clean time.

Usage: miiocli petfooddispenser reset_dryer_time [OPTIONS]

  Reset dryer time.

Usage: miiocli petfooddispenser set_feed_state [OPTIONS] STATE

  Enable / Disable the automatic feeding schedule.

Usage: miiocli petfooddispenser set_key_lock [OPTIONS] STATE

  Enable / Disable the key lock for the manual dispense button.

Usage: miiocli petfooddispenser set_wifi_led [OPTIONS] STATE

  Enable / Disable the wifi status led.

Will look into adding support for feedplans soon.

Feedback welcome.

Copy link
Owner

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

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

A very brief review, looking great! Please do not forget to add the device to README.md, too

miio/integrations/petfooddispenser/status.py Show resolved Hide resolved
miio/integrations/petfooddispenser/status.py Outdated Show resolved Hide resolved
miio/integrations/petfooddispenser/status.py Outdated Show resolved Hide resolved
miio/integrations/petfooddispenser/device.py Outdated Show resolved Hide resolved
miio/integrations/petfooddispenser/device.py Outdated Show resolved Hide resolved
@Wh1terat
Copy link
Author

Wh1terat commented Dec 6, 2021

Think that's everything fixed 😸

Comment on lines +43 to +50
"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",
Copy link
Owner

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).

Copy link
Author

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?

Copy link
Owner

@rytilahti rytilahti Dec 6, 2021

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?

miio/integrations/petfooddispenser/device.py Show resolved Hide resolved
return self.send("resetdryer")

@command(
click.argument("state", type=int),
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
click.argument("state", type=int),
click.argument("state", type=bool),

Use bool for boolean values across the board. This should work directly for the cli input values so no need for explicit conversions either.

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Owner

Choose a reason for hiding this comment

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

Oh, then casting is needed; int(boolvalue) will fix that.

Copy link
Author

Choose a reason for hiding this comment

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

🤦
Given I wrote it at 3am I'm amazed there's not more glaring issues. I'll sort that.

miio/integrations/petfooddispenser/status.py Outdated Show resolved Hide resolved
return self.send("outfood", [amount])

@command(default_output=format_output("Resetting clean time"))
def reset_clean_time(self) -> bool:
Copy link
Owner

Choose a reason for hiding this comment

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

Are you sure this returns a bool? And same for reset_dryer_time?

Copy link
Author

Choose a reason for hiding this comment

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

Good catch, it returns a list with a string of "ok".
That said, it returns "ok" literally no matter what you send to this device 🤦

Copy link
Owner

Choose a reason for hiding this comment

The 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.

return bool(self.data["door_status"])

@property
def clean_days(self) -> int:
Copy link
Owner

Choose a reason for hiding this comment

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

Use timedelta for this and the dryer one, this makes it consistent with other integrations.
Also, use naming scheme <variable>_left.

Copy link
Author

Choose a reason for hiding this comment

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

Perfect, was just about to ask about this.
"dryer_days" > "desiccant_left"

Can't quite think how to phrase "clean_days".
It's the number of days until the unit requires cleaning.
Suggestions?

Copy link
Owner

Choose a reason for hiding this comment

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

I don't have a good term for that either, time_until_clean_left is sounds quite verbose.

Copy link
Author

Choose a reason for hiding this comment

The 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.

lambda state: "Enabling automatic feeding schedule" if state else "Disabling automatic feeding schedule"
),
)
def set_feed_state(self, state: int):
Copy link
Owner

Choose a reason for hiding this comment

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

set_automated_feeding <-> automated_feeding in state, maybe?

Copy link
Author

Choose a reason for hiding this comment

The 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".

Copy link
Owner

Choose a reason for hiding this comment

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

Btw, you can run the linting tests locally by executing tox -e lint or pre-commit run -a (or alternatively, you can do pre-commit install and it will perform the checks prior commiting).

Copy link
Author

Choose a reason for hiding this comment

The 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 👼

@Wh1terat
Copy link
Author

Wh1terat commented Dec 6, 2021

Infact yeah, although this was an "initial support" pull - give me a day or so and I'll just refactor and add everything and make it fully supported from day one.

@Wh1terat
Copy link
Author

Work got a bit busy (chaos before Christmas change freeze!) but have started.

Few questions before we have another shot ;)

1. Implementing field maximums (i.e max dispense unit is 30)
Should this just be a logged error and return false from the relevant function or should I raise an exception? And if exception is it ok to use standard exceptions (i.e ValueError ) or should I subclass DeviceException (as I have seen in other modules).

2. Timezones!
I've implemented the feedplan schedules now. The device stores these as CST timezone, which isn't a problem for displaying as I've just made them into datetime objects with the appropriate tzinfo. (Bit annoying that datetime.time doesn't support astimezone, so having to make them full datetime.datetime objects with 1970,1,1 date 😠 )
But the big question, for adding a feedplan - what format would you suggest for the user to input this towards my function?

datetime.time with tzinfo?
datetime.datetime with tzinfo?
Hour, min as int and iana tz string?

3. Boolean responses
What's the general approach for boolean responses, just to return truthy falsy or to map these to string values?
Or create enums just for the 2 choices?
e.g:
On/Off
Locked/Unlocked
Open/Closed
Enabled/Disabled

@rytilahti
Copy link
Owner

Should this just be a logged error and return false from the relevant function or should I raise an exception? And if exception is it ok to use standard exceptions (i.e ValueError ) or should I subclass DeviceException (as I have seen in other modules).

Good catch, please use standard exceptions. I modified #1114 to list it as something that should be consolidated across the code base.

What's the general approach for boolean responses, just to return truthy falsy or to map these to string values?

Use booleans where possible, no need to enumize.

On the topic of timezones, I'm not sure what's the best approach here. IIRC the vacuum schedules do convert the times to the local tz, maybe take a look how it's done and follow the same approach?

@Wh1terat
Copy link
Author

Closing in favour of #1253

@Wh1terat Wh1terat closed this Dec 15, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants