-
-
Notifications
You must be signed in to change notification settings - Fork 558
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
Unified and scalable command line interface #191
Conversation
miio/click_common.py
Outdated
@@ -11,6 +11,9 @@ | |||
import ipaddress | |||
import miio | |||
import logging | |||
from typing import Union | |||
from functools import wraps | |||
from functools import partial |
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.
module level import not at top of file
miio/click_common.py
Outdated
@@ -11,6 +11,9 @@ | |||
import ipaddress | |||
import miio | |||
import logging | |||
from typing import Union | |||
from functools import wraps |
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.
module level import not at top of file
miio/click_common.py
Outdated
@@ -11,6 +11,9 @@ | |||
import ipaddress | |||
import miio | |||
import logging | |||
from typing import Union |
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.
module level import not at top of file
miio/airpurifier.py
Outdated
@@ -3,7 +3,10 @@ | |||
import re | |||
from typing import Any, Dict, Optional | |||
from collections import defaultdict | |||
from functools import wraps |
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.
'functools.wraps' imported but unused
@rytilahti @syssi could you take a look? Do you see any issues with that approach? I can start porting current command line features to use new approach, but it requires some effort to do that, so it would be nice to know if you like it. If not then I'll drop it. |
First of all I applaud that you have done this in a such short time (I have had this on my pipeline for a while with a similar metaclass approach, but never got into the semantics to make it really work), this is really awesome piece of work! In my earlier experiments I didn't want to introduce 'click' into the library parts, but I think this is unavoidable for the time being and not worth tinkering. Unfortunately I haven't had much spare time lately, so I must apologize for not answering your great write-up wrt async support. However, as I have wanted such a generalized cli for a longer time, I played around with this PR a bit (and implemented inheriting commands from base-classes, and added support for the plug, feel free to cherry-pick and modify as needed from https://github.com/rytilahti/python-miio/commits/common_cli_improvements), and all in all it feels good but could use some polisihing. Some discussion points:
About configuration handling:
This could be used to provide an interface such as I have some code creating simple config files based on backup files, but never got that far enough with my common interface hack to integrate it in. Using the model information would also pave a way for having model-specific interfaces (simply by overloading the commands on subclasses of the main one), if wanted :-) edit: one last thing I wanted to mention is, that there is also a need for generic cache file handling, as all of the devices (most likely) will require saving the sequence number. Or at least it would be a sane approach to take on that topic. So instead of having that handling inside the Vacuum class, it should be done for every type of supported devices. The vacuum one just extends that handling by having an extra sequence id for the manual control, maybe other devices could use some sort of extra state saving, too? edit2: this is really the last update, but I think it may be worth considering how easily can we extend this to support other types of output formats (see #98). |
@rytilahti thanks for the comments. For now I'll only address some points. I'll be away few days (urgent family matters) and then I'll try to go through it thoroughly.
|
@rytilahti regarding your edits:
|
@rytilahti what configuration format do you have in mind? Would you want to stick to something simple like built-in ini-file parser? Or maybe something more advanced like yaml? An example yaml config structure: devices:
ap1:
class: AirPurifier
ip: 1.2.3.4
token: asdf.... The
|
I really like 'mi', unfortunately it seems to be used by this: https://gentoo.com/di/ :(
Just wanted to point it out, at the moment I have no solution but you seem to be knowledgeable in the matter so I'll leave it to you for now :-)
I think this sounds the sanest, it'll also be fairly simple to provide a default implementation just for echoing the result I suppose.
This can be done by having default handlers for error and non-error cases, I think.
I somehow thought that those are "global" options, and should therefore belong to the top level. However after using your cli and implementing more devices to use it, I'm starting to feel that the way it's currently done is just fine.
See the above comment wrt. echo handlers. Adding a
They updated the vacuum's protocol at some point and made it much stricter, that's why the library bumps the sequence number of failures. I don't know if that's the case for rest of the devices, but having a way to map requests and responses can at least be useful with your asynchronous implementation later on.
In some cases (such as for status), I think it'd be enough just to have a way to get the raw presentation of the container as a dict. |
I prefer to avoid the built-in ini-file parser for a reason or another, but I don't really mind that much. Back in the days I was doing some tests with
which produces a yaml (and can be easily changed to produce inis or jsons, too) similar to this:
From the looks of it it seems that we think alike :-) I was merely adding the models instead of class names, as we have a mapping for this available (which we could move out from discovery to be more easily accessible).
This sounds like the winning solution to me! The location of the file can be reached with |
miio/plug.py
Outdated
@command( | ||
default_output=format_output("", | ||
"Power: {result.power}\n" | ||
"Temperature: {result.temperature}") |
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.
continuation line under-indented for visual indent
miio/plug.py
Outdated
|
||
class Plug(Device): | ||
"""Main class representing the smart wifi socket / plug.""" | ||
|
||
@command( | ||
default_output=format_output("", | ||
"Power: {result.power}\n" |
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.
continuation line under-indented for visual indent
miio/click_common.py
Outdated
|
||
def json_output(pretty=False): | ||
indent = 2 if pretty else None | ||
def decorator(func): |
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.
expected 1 blank line before a nested definition, found 0
miio/click_common.py
Outdated
from typing import Union | ||
from functools import wraps | ||
from functools import partial | ||
from .exceptions import DeviceError |
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.
module level import not at top of file
miio/click_common.py
Outdated
import json | ||
from typing import Union | ||
from functools import wraps | ||
from functools import partial |
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.
module level import not at top of file
miio/click_common.py
Outdated
@@ -11,6 +12,11 @@ | |||
import ipaddress | |||
import miio | |||
import logging | |||
import json | |||
from typing import Union | |||
from functools import wraps |
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.
module level import not at top of file
miio/click_common.py
Outdated
@@ -11,6 +12,11 @@ | |||
import ipaddress | |||
import miio | |||
import logging | |||
import json | |||
from typing import Union |
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.
module level import not at top of file
@@ -11,6 +12,11 @@ | |||
import ipaddress | |||
import miio | |||
import logging | |||
import json |
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.
module level import not at top of file
I've changed how the output decorator is applied to the command decorator. It now needs to be passed as a I've also implemented an output option for top level cli group and created an output attribute in the GlobalContextObject class, which can pass any decorator which will override the default one. As an example I've implemented the json_output decorator and added output option to top level cli group which allows switching to json or json_pretty output. |
Btw, not all dicts are json serializable, but I suppose we are not going to have structures which are not, so I think it's okay. Do you think we could start moving towards merging this to master (after the next bugfix release) and fix the issues as they come? We can either keep the existing tools in place, or create simplified wrappers to keep the interfaces intact. I'd prefer the latter as that'd allow us to clean up the code-base. |
The json_output decorator can be enhanced by creating a class inheriting from I need to do some tests to check if this approach can be mixed with asyncio, to be sure that this whole PR won't block switching to async. |
Wrt JSONEncoder, I just mentioned it as the dunder is called |
Yeah, it's a naming issue. Maybe it should be called |
Maybe |
from typing import Union | ||
from functools import wraps | ||
from functools import partial | ||
from .exceptions import DeviceError |
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.
module level import not at top of file
import re | ||
from typing import Union | ||
from functools import wraps | ||
from functools import partial |
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.
module level import not at top of file
import json | ||
import re | ||
from typing import Union | ||
from functools import wraps |
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.
module level import not at top of file
Sorry for my recent absence. I've rebased the code on the current master branch and I've added better support for enum args (based on the code posted in pallets/click#605). The new EnumType is used in AirPurifier class. What about the command name? |
Not a thing, welcome back! The name question is quite tricky, |
What about miiocli or miioctl? |
👍 for miiocli, we shall still keep the existing |
@yawor did you see the last comment? I would like to merge the feature as soon as possible. |
No need to specify it in subclasses.
Code provided by skycaptain in pallets/click#605
miio/plug.py
Outdated
@command( | ||
default_output=format_output("", | ||
"Power: {result.power}\n" | ||
"Temperature: {result.temperature}") |
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.
continuation line under-indented for visual indent
miio/plug.py
Outdated
|
||
@command( | ||
default_output=format_output("", | ||
"Power: {result.power}\n" |
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.
continuation line under-indented for visual indent
miio/plug.py
Outdated
@@ -0,0 +1,76 @@ | |||
import logging | |||
from typing import Dict, Any, Optional |
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.
'typing.Optional' imported but unused
def usb_on(self): | ||
"""Power on.""" | ||
return self.send("set_usb_on", []) | ||
|
||
@command( | ||
default_output = format_output("Powering USB off"), |
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.
unexpected spaces around keyword / parameter equals
def off(self): | ||
"""Power off.""" | ||
if self.model == MODEL_CHUANGMI_PLUG_V1: | ||
return self.send("set_off", []) | ||
|
||
return self.send("set_power", ["off"]) | ||
|
||
@command( | ||
default_output = format_output("Powering USB on"), |
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.
unexpected spaces around keyword / parameter equals
def on(self): | ||
"""Power on.""" | ||
if self.model == MODEL_CHUANGMI_PLUG_V1: | ||
return self.send("set_on", []) | ||
|
||
return self.send("set_power", ["on"]) | ||
|
||
@command( | ||
default_output = format_output("Powering off"), |
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.
unexpected spaces around keyword / parameter equals
@@ -123,28 +137,47 @@ def status(self) -> ChuangmiPlugStatus: | |||
return ChuangmiPlugStatus( | |||
defaultdict(lambda: None, zip(properties, values))) | |||
|
|||
@command( | |||
default_output = format_output("Powering on"), |
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.
unexpected spaces around keyword / parameter equals
Hi guys. I'm sorry for my absence. It's awesome seeing this PR merged already and also extended by adding support in other device types :). I hope I'll be able to get back to help making this library better soon. Asyncio support waits for some love :D. |
I think that the current approach to command line interface, where different types of devices have separate commands has a lot of code duplication and is not very scalable. Right now only four device classes are even directly supported. Making more classes supported would mean more
*_cli.py
files making even harder to maintain.Here's my proposition. I've created a single entry point in
miio.cli
, which is registered asmiio
command during setup. I've played around with some metaclass programming and click to create sub-commands auto-registration mechanism. I've added glue logic to theAirPurifier
andVacuum
classes (Vacuum
is WIP).Main elements are the
DeviceGroupMeta
meta class which needs to be added to the specific device class anddevice_command
decorator, which itself takes a decorators collection in form of*args
. This is something I'm calling a lazy decorator application (couldn't find a better name for it :)). The decorators in the collection are not applied and no click commands are created unless actually used frommiio.cli
.This means that the classes can still be used directly by other software without any changes but the classes are augmented when used through the new command line
miio
.An example usage:
miio airpurifier --ip <some ip> --token <some token> status
miio airpurifier --ip <some ip> --token <some token> set_mode auto
miio vacuum --ip <some ip> --token <some token> status
etc
I've also enabled auto envvar prefix in Click, so the ip and token can be set for each device class like this:
There's also an example of Group customisation in
Vacuum
class in the form ofget_device_group
class method. When that method is present, it's responsible for producing a command group. In the example I've re-implemented the sequence file loading and saving.Please don't hesitate to comment, criticise etc. It's still WIP so I'm open to suggestions.