rytilahti / python-miio

Python library & console tool for controlling Xiaomi smart appliances
https://python-miio.readthedocs.io
GNU General Public License v3.0
3.7k stars 554 forks source link

Common Vacuum Interface #1059

Open stek29 opened 3 years ago

stek29 commented 3 years ago

Currently this library provides at least three different implementations for Vacuums (roborock, dreame and viomi) which are incompatible. As mentioned in home-assistant/core#49188, best way to get different Vacuums into other projects (like Home Assistant) is to provide some kind of common interface for vacuums.

I see three ways to do that:

  1. create "abstract" interface for vacuums, then refactor existing vacuums to support that interface – might be backward incompatible?
  2. create "abstract" intrerface for vacuums, create implementations of that interface for each vacuum type to keep backward compatibility; if any new types of vacuums are added, they should implement that interface directly
  3. create common vacuum interface which acts as an adaptor for exact implementations: might be harder to maintain in future due to centralisation, but keeps backward compatibilty

Also, it'd be great to have some kind of "router" which automatically chooses correct implementation depending on device type – the easiest way to do that is by going with third way

What do you think? I'd be more than happy to make this happen, but it needs to be discussed and thought through first

Also, if I'm not mistaken, there's currently no common interfaces for any kind of device – we should keep that in mind when designing this, so solution is scalable to other kinds of devices.

rytilahti commented 3 years ago

Hi, yes, I think it would be great to common interfaces per device type (similarly how homeassistant has platforms) but unfortunately I haven't had time to tinker with it and won't be able to do that in the near future either.

What I had in mind is to have a class hierarchy that defines the minimal common interface and to allow device-specific classes extend that if they wish to do so. IMO your second proposal is the way to go for a release or two, while marking the non-standardized APIs as deprecated so in case anyone is using them will get a notice before they get removed after some time.

Here is an example from some old notes I wrote down while looking into the vacuum classes while wondering about the common API:

class StatusContainer:
    """Common base-class for all status containers."""
    def is_on():
        """Returns if the device is on."""
    def got_error():
        """Returns if the device is in error state."""

class VacuumStatusContainer(StatusContainer):
    """Common base class for all vacuums."""
    def battery() -> int:
        """Battery charge state."""
    def state_code() -> int:
        """Code of the current vacuum state."""
    def state() -> str:
        """Human readable presentation of the state."""
    def error_code() -> Optional[int]:
        """Error code, if errored."""
    def error() -> Optional[str]:
        """Human readable presentation of the error."""
    def fanspeed() -> int:
        """Fan-speed.."""

class Device:
    def status() -> StatusContainer:
        """All devices should support status container."""

def BaseVacuum(Device):
    def start(): ...

    def pause(): ...

    def stop(): ...

    def status(): ...

    def home(): ...

    def status() -> VacuumStatusContainer:
        pass

    def find():
        """Action to ask vacuum to yield its location"""

    def consumable_status() -> VacuumConsumableStatus:
        pass

    def clean_history() -> VacuumCleanSummary:
        pass

    def clean_details(int: id_) -> VacuumCleanDetails:
        pass

    def set_fan_speed(speed: Union[VacuumFanSpeed,int]):
        """Set fan speed."""

    # VacuumFanSpeedPresets could be simply a Dict[str, int]
    def fan_speed_presets() -> VacuumFanSpeedPresets:
        pass

    def supported_features() -> VacuumFeatures:
        """A enumflag? to indicate available features."""

As you can see, it is not anywhere near complete so that's why I haven't put it up as a RFC issue. However, as you mentioned you would be interested in working on this, I think it's fair enough to put it up here :-) Some parts like status_code reporting could be something that might be useful to have on the base status container among other things, consider this just a very raw draft of how it could work potentially.

Also, it'd be great to have some kind of "router" which automatically chooses correct implementation depending on device type – the easiest way to do that is by going with third way

This is another topic altogether, but shortly put, I think that when/if there will be device-specific base classes, the specific implementations should be moved from the main source directory to their separate directories:

miio/
  basevacuum.py
  vacuum/
    xiaomi/
        vacuum.py (implements the basevacuum)
        info.yaml
    dreamevacuum/
       vacuum.py
       info.yaml

And so on for each device types that are standardized. The info.yaml should contain information about the device (including the device models supported by that "plugin"), that will allow automatic initialization of the wanted implementation based on the info() query results (related to #1038).

On related note, that would open up a way to have a plugin-based system that would also allow external implementations, e.g., by using pluggy (https://pluggy.readthedocs.io/en/latest/) or something similar but simpler that allows defining these plugins using the python entry points for discovery. However, this should be discussed separately from the base class APIs in a separate issue, but having standardized APIs would make that feasible to pull off.

So, if you wish to work on this, I think the vacuums are a good candidate for experimentation and hope my notes above will help with that :-)