rytilahti / python-miio

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

Mi Robot Vacuum-Mop Essential (G1) #817

Closed Alangasar closed 3 years ago

Alangasar commented 4 years ago

Before submitting a new request, use the search to see if there is an existing issue for the device.

Device information:

Use miiocli device --ip <ip address> --token <token>.

Model: mijia.vacuum.v1 Hardware version: esp32 Firmware version: 2.1.3

amp92 commented 4 years ago

Has anyone tested the commands on this robot?

amp92 commented 4 years ago

I tested the commands and found some working.

get_properties:

piid: 2 siid: 1 - device status piid: 2 siid: 2 - I don't know

piid: 3 siid: 1 - battery in% piid: 3 siid: 2 - battery status

piid: 4 siid: 1 - voice status piid: 4 siid: 2 - voice volume in%

piid: 9 siid: 1 - last vacuum cleaning area in m2 piid: 9 siid: 2 - duration of the last vacuuming in min piid: 9 siid: 3 - total cleaning area in m2 piid: 9 siid: 4 - total duration of cleaning in min piid: 9 siid: 5 - total number of vacuums

piid: 11 siid: 1 - filter consumption in% piid: 11 siid: 2 - remaining time to replace the filter in min

piid: 12 siid: 1 - I don't know piid: 12 siid: 2 - DND mode state

piid: 14 siid: 1 - main brush consumption in% piid: 14 siid: 2 - remaining time to replace the main brush in min

piid: 15 siid: 1 - angle brush wear in% piid: 15 siid: 2 - remaining time to replace angular brushes in min

piid: 16 siid: 1 - value 0, I don't know

set_properties:

siid: 4 piid: 1 value: 0/1 enable disable voice siid: 4 piid: 2 value: volume value 0-100 siid: 8 piid: 1 value: 2 - a step forward; 0 - left; 1 - right; -- > Captured packet from Mi Home by wireshark, UDP protocol. I couldn't find the command to start - stop the robot and setting the vacuuming power. I've been capturing packets and the Mi Home app doesn't send packets directly to the robot. It only sends data to the server via the TLS protocol.

This is an example of sending a request and response: Request: { "method": "get_properties", "params": [ { "did": "123456789", "siid": 2, "piid": 1 } ] } Response: { "id": 367107977, "result": [ { "did": "123456789", "siid": 2, "piid": 1, "code": 0, "value": 5 } ], "exe_time": 70 }

There is someone who can help me??? The most important thing for me is turning the robot on and off.

rezmus commented 4 years ago

Start cleanup "method":"action","params":{"did":"X","siid":2,"aiid":1} Pause cleanup "method":"action","params":{"did":"X","siid":2,"aiid":2} Charge "method":"action","params":{"did":"X","siid":13,"aiid":1}

amp92 commented 4 years ago

Start cleanup "method":"action","params":{"did":"X","siid":2,"aiid":1} Pause cleanup "method":"action","params":{"did":"X","siid":2,"aiid":2} Charge "method":"action","params":{"did":"X","siid":13,"aiid":1}

It works, thanks.

burjakremen commented 4 years ago

I tested the commands and found some working.

get_properties:

piid: 2 siid: 1 - device status piid: 2 siid: 2 - I don't know

piid: 3 siid: 1 - battery in% piid: 3 siid: 2 - battery status

piid: 4 siid: 1 - voice status piid: 4 siid: 2 - voice volume in%

piid: 9 siid: 1 - last vacuum cleaning area in m2 piid: 9 siid: 2 - duration of the last vacuuming in min piid: 9 siid: 3 - total cleaning area in m2 piid: 9 siid: 4 - total duration of cleaning in min piid: 9 siid: 5 - total number of vacuums

piid: 11 siid: 1 - filter consumption in% piid: 11 siid: 2 - remaining time to replace the filter in min

piid: 12 siid: 1 - I don't know piid: 12 siid: 2 - DND mode state

piid: 14 siid: 1 - main brush consumption in% piid: 14 siid: 2 - remaining time to replace the main brush in min

piid: 15 siid: 1 - angle brush wear in% piid: 15 siid: 2 - remaining time to replace angular brushes in min

piid: 16 siid: 1 - value 0, I don't know

set_properties:

siid: 4 piid: 1 value: 0/1 enable disable voice siid: 4 piid: 2 value: volume value 0-100 siid: 8 piid: 1 value: 2 - a step forward; 0 - left; 1 - right; -- > Captured packet from Mi Home by wireshark, UDP protocol. I couldn't find the command to start - stop the robot and setting the vacuuming power. I've been capturing packets and the Mi Home app doesn't send packets directly to the robot. It only sends data to the server via the TLS protocol.

This is an example of sending a request and response: Request: { "method": "get_properties", "params": [ { "did": "123456789", "siid": 2, "piid": 1 } ] } Response: { "id": 367107977, "result": [ { "did": "123456789", "siid": 2, "piid": 1, "code": 0, "value": 5 } ], "exe_time": 70 }

There is someone who can help me??? The most important thing for me is turning the robot on and off.

get_commands = { 1:'{"method":"get_properties","params":[{"siid":2,"piid":1}]}', #command_get_status 2:'{"method":"get_properties","params":[{"siid":2,"piid":2}]}', #command_get_error 3:'{"method":"get_properties","params":[{"siid":2,"piid":4}]}', #command_get_mode 4:'{"method":"get_properties","params":[{"siid":2,"piid":5}]}', #command_get_water_mode 5:'{"method":"get_properties","params":[{"siid":2,"piid":6}]}', #command_get_fan_mode 6:'{"method":"get_properties","params":[{"siid":3,"piid":2}]}', #command_get_charge_state 7:'{"method":"get_properties","params":[{"siid":16,"piid":1}]}', #command_get_mop_status 8:'{"method":"get_properties","params":[{"siid":3,"piid":1}]}', #command_get_battery_level 9:'{"method":"get_properties","params":[{"siid":14,"piid":1}]}', #command_get_main_brush_life_level_percent 10:'{"method":"get_properties","params":[{"siid":14,"piid":2}]}', #command_get_main_brush_life_level_minutes 11:'{"method":"get_properties","params":[{"siid":15,"piid":1}]}', #command_get_side_brush_life_level_percent 12:'{"method":"get_properties","params":[{"siid":15,"piid":2}]}', #command_get_side_brush_life_level_minutes 13:'{"method":"get_properties","params":[{"siid":11,"piid":1}]}', #command_get_filter_life_level_percent 14:'{"method":"get_properties","params":[{"siid":11,"piid":2}]}', #command_get_filter_brush_life_level_minutes 15:'{"method":"get_properties","params":[{"siid":9,"piid":1}]}', #command_get_clean-area # responce value always 0 16:'{"method":"get_properties","params":[{"siid":9,"piid":2}]}', #command_get_clean-time # responce value always 0 17:'{"method":"get_properties","params":[{"siid":9,"piid":3}]}', #command_get_total_clean-area # responce value always 0 18:'{"method":"get_properties","params":[{"siid":9,"piid":4}]}', #command_get_total_clean-time # responce value always 0 19:'{"method":"get_properties","params":[{"siid":9,"piid":5}]}', #command_get_total_clean-count # responce value always 0 20:'{"method":"get_properties","params":[{"siid":12,"piid":1}]}', #command_get_speech_language 21:'{"method":"get_properties","params":[{"siid":12,"piid":1}]}'} #command_get_DND_status

action_commands = { 1:'{"method":"action","params":{"siid":2,"aiid":1}}', #command_start_sweep 2:'{"method":"action","params":{"siid":2,"aiid":2}}', #command_stop_sweep 3:'{"method":"action","params":{"siid":2,"aiid":3}}', #command_go_charge 4:'{"method":"action","params":{"siid":6,"aiid":1}}', #command_find_vacuum 5:'{"method":"action","params":{"siid":13,"aiid":1}}', #command_start_charge 6:'{"method":"action","params":{"siid":13,"aiid":2}}'} #command_stop_charge

set_commands = { 1:'{"method":"set_properties","params":[{"siid":2,"piid":4,"value":', #command_set_work_mode 2:'{"method":"set_properties","params":[{"siid":2,"piid":5,"value":', #command_set_water_mode 3:'{"method":"set_properties","params":[{"siid":2,"piid":6,"value":', #command_set_fan_mode 4:'{"method":"set_properties","params":[{"siid":12,"piid":1,"value":', #command_set_speech_language 5:'{"method":"set_properties","params":[{"siid":12,"piid":2,"value":'} #command_set_DND_status

amp92 commented 4 years ago

Thanks. I've already tested all these commands from this link: https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:vacuum:0000A006:mijia-v1:1

burjakremen commented 4 years ago

Thanks. I've already tested all these commands from this link: https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:vacuum:0000A006:mijia-v1:1

19:_'{"method":"getproperties","params":[{"siid":9,"piid":5}]}', #command_get_total_clean-count # responce value always 0

What kind of response you have received this request?

amp92 commented 4 years ago

Thanks. I've already tested all these commands from this link: https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:vacuum:0000A006:mijia-v1:1

19:_'{"method":"getproperties","params":[{"siid":9,"piid":5}]}', #command_get_total_clean-count # responce value always 0

What kind of response you have received this request?

When I was using the vacuum cleaner by MiHome I had values ​​there. Now, when I started the vacuum cleaner by commands, I also always have 0. Try to run the vacuum cleaner through MiHome several times and check if the values ​​are there.

WildeRNS commented 3 years ago

Now, when I started the vacuum cleaner by commands, I also always have 0

Can you write how to run commands? I only want to run/stop cleaner from command line. Thanks

amp92 commented 3 years ago

Now, when I started the vacuum cleaner by commands, I also always have 0

Can you write how to run commands? I only want to run/stop cleaner from command line. Thanks

Where do you want to run these commands?

WildeRNS commented 3 years ago

Where do you want to run these commands?

From command line. I want make script for Home Assistant.

amp92 commented 3 years ago

From command line. I want make script for Home Assistant.

I use the plugin for domoticz from here: https://github.com/mrin/domoticz-mirobot-plugin

RickHagendijk commented 3 years ago

I see that this topic is quite Inactive nowadays but I am wondering whether the Xiaomi Robot Vacuum-Mop Essential is (fully) supported or not. This because I om thinking about buying one as my first vacuum robot and I want to use it in combination with Domoticz using this library.

TijnvanGrinsven commented 3 years ago

Hey Rick, I have spend 2 days trying to get it to work with hass but the essential mob is not yet fully supported. Compared this with a roborock of a friend of mine and that integrations works like a charm. I recommend you look for a different mob if you want full support.

NL647 commented 3 years ago

Hello, is there any news about support for this device?

amp92 commented 3 years ago

Hello, is there any news about support for this device?

I am using a custom component for home assistant based on python-miio.

Class for sending commands.

from .miot_device import MiotDevice

class MyVacuum(MiDevice):

    @command()
    def status(self) -> Status:
        status = Status()
        response = self.send('get_properties', [{"siid": 2, "piid": 1}, {"siid": 2, "piid": 2}, {"siid": 2, "piid": 6}, {"siid": 3, "piid": 1}, {"siid": 9, "piid": 2}, {"siid": 9, "piid": 1}, {"siid": 14, "piid": 1}, {"siid": 15, "piid": 1}, {"siid": 11, "piid": 1}])

        status.error = response[1]["value"]
        status.status = response[0]["value"]
        status.battery = response[3]["value"]
        status.fan_speed = response[2]["value"]
        status.clean_time = response[4]["value"]
        status.clean_area = response[5]["value"]

        status.brush_life_level = response[6]["value"]
        status.side_brush_life_level = response[7]["value"]
        status.filter_life_level = response[8]["value"]

        return status

    def call_action(self, siid, aiid, params=None):
        if params is None:
            params = []
        payload = {
            "did": f"call-{siid}-{aiid}",
            "siid": siid,
            "aiid": aiid,
        }
        return self.send("action", payload)

    @command(click.argument("speed", type=int))
    def set_fan_speed(self, speed):
        """Set fan speed"""
        return self.send('set_properties', [{"siid": 2, "piid": 6, "value": speed}])

    @command()
    def return_home(self) -> None:
        """aiid 1 Start Charge: in: [] -> out: []"""
        return self.call_action(2, 3) 

    @command()
    def start(self) -> None:
        """Start cleaning."""
        return self.call_action(2, 1)

    @command()
    def stop(self) -> None:
        """Stop cleaning."""
        return self.call_action(2, 2)

    @command()
    def find(self) -> None:
        """Find the robot."""
        return self.call_action(6, 1)

Below modified vacuum class in ...config\custom_components\xiaomi_vacuum.

"""Xiaomi Vacuum"""
from functools import partial
import logging
import voluptuous as vol

from .miio import MyVacuum, DeviceException

from homeassistant.components.vacuum import (
    PLATFORM_SCHEMA,
    SUPPORT_STATE,
    SUPPORT_BATTERY,
    SUPPORT_LOCATE,
    SUPPORT_PAUSE,
    SUPPORT_RETURN_HOME,
    SUPPORT_START,
    SUPPORT_STOP,
    SUPPORT_FAN_SPEED,
    STATE_CLEANING,
    STATE_IDLE,
    STATE_PAUSED,
    STATE_RETURNING,
    STATE_DOCKED,
    STATE_ERROR,
    StateVacuumEntity,
)

from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.helpers import config_validation as cv, entity_platform

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = "Xiaomi Vacuum cleaner"
DATA_KEY = "vacuum.xiaomi_vacuum"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_HOST): cv.string,
        vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    },
    extra=vol.ALLOW_EXTRA,
)

ATTR_STATUS = "status"
ATTR_ERROR = "error"
ATTR_FAN_SPEED = "fan_speed"
ATTR_CLEANING_TIME = "cleaning_time"
ATTR_CLEANING_AREA = "cleaning_area"
ATTR_MAIN_BRUSH_LIFE_LEVEL = "main_brush_life_level"
ATTR_SIDE_BRUSH_LIFE_LEVEL = "side_brush_life_level"
ATTR_FILTER_LIFE_LEVEL = "filter_life_level"

SUPPORT_XIAOMI = (
    SUPPORT_STATE
    | SUPPORT_BATTERY
    | SUPPORT_LOCATE
    | SUPPORT_RETURN_HOME
    | SUPPORT_START
    | SUPPORT_STOP
    | SUPPORT_PAUSE
    | SUPPORT_FAN_SPEED
)

STATE_CODE_TO_STATE = {
    1: STATE_IDLE,
    2: STATE_CLEANING,
    3: STATE_PAUSED,
    4: STATE_ERROR,
    5: STATE_DOCKED,
    6: STATE_RETURNING,
}

SPEED_CODE_TO_NAME = {
    0: "Silent",
    1: "Standard",
    2: "Strong",
    3: "Turbo",
}

ERROR_CODE_TO_ERROR = {  
    0: "NoError",
    1: "Left_wheel_motor",
    2: "Right_wheel_motor",
    3: "Cliff",
    4: "Battery_low",
    5: "Bumper",
    6: "Brush",
    7: "Side_brush",
    8: "Fan",
    9: "Dustbin",
    10: "Charging",
    11: "No_wate",
    12: "Pick_up",
}

async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Set up the Xiaomi vacuum cleaner robot platform."""
    if DATA_KEY not in hass.data:
        hass.data[DATA_KEY] = {}

    host = config.get(CONF_HOST)
    token = config.get(CONF_TOKEN)
    name = config.get(CONF_NAME)

    # Create handler
    _LOGGER.info("Initializing with host %s (token %s...)", host, token)
    vacuum = MyVacuum(host, token)

    mirobo = MiroboVacuum(name, vacuum)
    hass.data[DATA_KEY][host] = mirobo

    async_add_entities([mirobo], update_before_add=True)

class MiroboVacuum(StateVacuumEntity):
    """Representation of a Xiaomi Vacuum cleaner robot."""

    def __init__(self, name, vacuum):
        """Initialize the Xiaomi vacuum cleaner robot handler."""
        self._name = name
        self._vacuum = vacuum

        self._fan_speeds = None
        self._fan_speeds_reverse = None

        self.vacuum_state = None
        self.vacuum_error = None
        self.battery_percentage = None

        self._current_fan_speed = None

        self._main_brush_life_level = None

        self._side_brush_life_level = None

        self._filter_life_level = None

        self._cleaning_area = None
        self._cleaning_time = None      

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def state(self):
        """Return the status of the vacuum cleaner."""
        if self.vacuum_state is not None:
            try:
                return STATE_CODE_TO_STATE[int(self.vacuum_state)]
            except KeyError:
                _LOGGER.error(
                    "STATE_CODE not supported: %s",
                    self.vacuum_state,
                )
                return None

    @property
    def error(self):
        """Return the error of the vacuum cleaner."""
        if self.vacuum_error is not None:
            try:
                return ERROR_CODE_TO_ERROR.get(self.vacuum_error, "Unknown")
            except KeyError:
                _LOGGER.error(
                    "ERROR_CODE not supported: %s",
                    self.vacuum_error,
                )
                return None

    @property
    def battery_level(self):
        """Return the battery level of the vacuum cleaner."""
        if self.vacuum_state is not None:
            return self.battery_percentage

    @property
    def fan_speed(self):
        """Return the fan speed of the vacuum cleaner."""
        if self.vacuum_state is not None:
            speed = self._current_fan_speed
            if speed in self._fan_speeds_reverse:
                return SPEED_CODE_TO_NAME.get(self._current_fan_speed, "Unknown")

            _LOGGER.debug("Unable to find reverse for %s", speed)

            return speed

    @property
    def fan_speed_list(self):
        """Get the list of available fan speed steps of the vacuum cleaner."""
        return list(self._fan_speeds_reverse)

    @property
    def device_state_attributes(self):
        """Return the specific state attributes of this vacuum cleaner."""
        if self.vacuum_state is not None:
            return {
                ATTR_STATUS: STATE_CODE_TO_STATE[int(self.vacuum_state)],
                ATTR_ERROR:  ERROR_CODE_TO_ERROR.get(self.vacuum_error, "Unknown"),
                ATTR_FAN_SPEED: SPEED_CODE_TO_NAME.get(self._current_fan_speed, "Unknown"),
                ATTR_MAIN_BRUSH_LIFE_LEVEL: self._main_brush_life_level,
                ATTR_SIDE_BRUSH_LIFE_LEVEL: self._side_brush_life_level,
                ATTR_FILTER_LIFE_LEVEL: self._filter_life_level,
                ATTR_CLEANING_AREA: self._cleaning_area,
                ATTR_CLEANING_TIME: self._cleaning_time,                
            } 

    @property
    def supported_features(self):
        """Flag vacuum cleaner robot features that are supported."""
        return SUPPORT_XIAOMI

    async def _try_command(self, mask_error, func, *args, **kwargs):
        """Call a vacuum command handling error messages."""
        try:
            await self.hass.async_add_executor_job(partial(func, *args, **kwargs))
            return True
        except DeviceException as exc:
            _LOGGER.error(mask_error, exc)
            return False

    async def async_locate(self, **kwargs):
        """Locate the vacuum cleaner."""
        await self._try_command("Unable to locate the botvac: %s", self._vacuum.find)

    async def async_start(self):
        """Start or resume the cleaning task."""
        await self._try_command(
            "Unable to start the vacuum: %s", self._vacuum.start)

    async def async_stop(self, **kwargs):
        """Stop the vacuum cleaner."""
        await self._try_command("Unable to stop: %s", self._vacuum.stop)

    async def async_pause(self):
        """Pause the cleaning task."""
        await self._try_command("Unable to set start/pause: %s", self._vacuum.stop)

    async def async_return_to_base(self, **kwargs):
        """Set the vacuum cleaner to return to the dock."""
        await self._try_command("Unable to return home: %s", self._vacuum.return_home)

    async def async_set_fan_speed(self, fan_speed, **kwargs):
        """Set fan speed."""
        if fan_speed in self._fan_speeds_reverse:
            fan_speed = self._fan_speeds_reverse[fan_speed]
        else:
            try:
                fan_speed = int(fan_speed)
            except ValueError as exc:
                _LOGGER.error(
                    "Fan speed step not recognized (%s). Valid speeds are: %s",
                    exc,
                    self.fan_speed_list,
                )
                return
        await self._try_command(
            "Unable to set fan speed: %s", self._vacuum.set_fan_speed, fan_speed)    

    def update(self):
        """Fetch state from the device."""
        try:
            state = self._vacuum.status()
            self.vacuum_state = state.status
            self.vacuum_error = state.error

            self._fan_speeds = SPEED_CODE_TO_NAME
            self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()}

            self.battery_percentage = state.battery

            self._current_fan_speed = state.fan_speed

            self._main_brush_life_level = state.brush_life_level

            self._side_brush_life_level = state.side_brush_life_level

            self._filter_life_level = state.filter_life_level

            self._cleaning_area = state.clean_area
            self._cleaning_time = state.clean_time

        except OSError as exc:
            _LOGGER.error("Got OSError while fetching the state: %s", exc) 
WildeRNS commented 3 years ago

In Home Assistant you can integrate cleaner with custom MioT Auto integration (available in HACS)

NL647 commented 3 years ago

In Home Assistant you can integrate cleaner with custom MioT Auto integration (available in HACS)

Perfect thank you!

NL647 commented 3 years ago

Hello, is there any news about support for this device?

I am using a custom component for home assistant based on python-miio.

Class for sending commands.

from .miot_device import MiotDevice

class MyVacuum(MiDevice):

    @command()
    def status(self) -> Status:
        status = Status()
        response = self.send('get_properties', [{"siid": 2, "piid": 1}, {"siid": 2, "piid": 2}, {"siid": 2, "piid": 6}, {"siid": 3, "piid": 1}, {"siid": 9, "piid": 2}, {"siid": 9, "piid": 1}, {"siid": 14, "piid": 1}, {"siid": 15, "piid": 1}, {"siid": 11, "piid": 1}])

        status.error = response[1]["value"]
        status.status = response[0]["value"]
        status.battery = response[3]["value"]
        status.fan_speed = response[2]["value"]
        status.clean_time = response[4]["value"]
        status.clean_area = response[5]["value"]

        status.brush_life_level = response[6]["value"]
        status.side_brush_life_level = response[7]["value"]
        status.filter_life_level = response[8]["value"]

        return status

    def call_action(self, siid, aiid, params=None):
        if params is None:
            params = []
        payload = {
            "did": f"call-{siid}-{aiid}",
            "siid": siid,
            "aiid": aiid,
        }
        return self.send("action", payload)

    @command(click.argument("speed", type=int))
    def set_fan_speed(self, speed):
        """Set fan speed"""
        return self.send('set_properties', [{"siid": 2, "piid": 6, "value": speed}])

    @command()
    def return_home(self) -> None:
        """aiid 1 Start Charge: in: [] -> out: []"""
        return self.call_action(2, 3) 

    @command()
    def start(self) -> None:
        """Start cleaning."""
        return self.call_action(2, 1)

    @command()
    def stop(self) -> None:
        """Stop cleaning."""
        return self.call_action(2, 2)

    @command()
    def find(self) -> None:
        """Find the robot."""
        return self.call_action(6, 1)

Below modified vacuum class in ...config\custom_components\xiaomi_vacuum.

"""Xiaomi Vacuum"""
from functools import partial
import logging
import voluptuous as vol

from .miio import MyVacuum, DeviceException

from homeassistant.components.vacuum import (
    PLATFORM_SCHEMA,
    SUPPORT_STATE,
    SUPPORT_BATTERY,
    SUPPORT_LOCATE,
    SUPPORT_PAUSE,
    SUPPORT_RETURN_HOME,
    SUPPORT_START,
    SUPPORT_STOP,
    SUPPORT_FAN_SPEED,
    STATE_CLEANING,
    STATE_IDLE,
    STATE_PAUSED,
    STATE_RETURNING,
    STATE_DOCKED,
    STATE_ERROR,
    StateVacuumEntity,
)

from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.helpers import config_validation as cv, entity_platform

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = "Xiaomi Vacuum cleaner"
DATA_KEY = "vacuum.xiaomi_vacuum"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_HOST): cv.string,
        vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    },
    extra=vol.ALLOW_EXTRA,
)

ATTR_STATUS = "status"
ATTR_ERROR = "error"
ATTR_FAN_SPEED = "fan_speed"
ATTR_CLEANING_TIME = "cleaning_time"
ATTR_CLEANING_AREA = "cleaning_area"
ATTR_MAIN_BRUSH_LIFE_LEVEL = "main_brush_life_level"
ATTR_SIDE_BRUSH_LIFE_LEVEL = "side_brush_life_level"
ATTR_FILTER_LIFE_LEVEL = "filter_life_level"

SUPPORT_XIAOMI = (
    SUPPORT_STATE
    | SUPPORT_BATTERY
    | SUPPORT_LOCATE
    | SUPPORT_RETURN_HOME
    | SUPPORT_START
    | SUPPORT_STOP
    | SUPPORT_PAUSE
    | SUPPORT_FAN_SPEED
)

STATE_CODE_TO_STATE = {
    1: STATE_IDLE,
    2: STATE_CLEANING,
    3: STATE_PAUSED,
    4: STATE_ERROR,
    5: STATE_DOCKED,
    6: STATE_RETURNING,
}

SPEED_CODE_TO_NAME = {
    0: "Silent",
    1: "Standard",
    2: "Strong",
    3: "Turbo",
}

ERROR_CODE_TO_ERROR = {  
    0: "NoError",
    1: "Left_wheel_motor",
    2: "Right_wheel_motor",
    3: "Cliff",
    4: "Battery_low",
    5: "Bumper",
    6: "Brush",
    7: "Side_brush",
    8: "Fan",
    9: "Dustbin",
    10: "Charging",
    11: "No_wate",
    12: "Pick_up",
}

async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Set up the Xiaomi vacuum cleaner robot platform."""
    if DATA_KEY not in hass.data:
        hass.data[DATA_KEY] = {}

    host = config.get(CONF_HOST)
    token = config.get(CONF_TOKEN)
    name = config.get(CONF_NAME)

    # Create handler
    _LOGGER.info("Initializing with host %s (token %s...)", host, token)
    vacuum = MyVacuum(host, token)

    mirobo = MiroboVacuum(name, vacuum)
    hass.data[DATA_KEY][host] = mirobo

    async_add_entities([mirobo], update_before_add=True)

class MiroboVacuum(StateVacuumEntity):
    """Representation of a Xiaomi Vacuum cleaner robot."""

    def __init__(self, name, vacuum):
        """Initialize the Xiaomi vacuum cleaner robot handler."""
        self._name = name
        self._vacuum = vacuum

        self._fan_speeds = None
        self._fan_speeds_reverse = None

        self.vacuum_state = None
        self.vacuum_error = None
        self.battery_percentage = None

        self._current_fan_speed = None

        self._main_brush_life_level = None

        self._side_brush_life_level = None

        self._filter_life_level = None

        self._cleaning_area = None
        self._cleaning_time = None        

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def state(self):
        """Return the status of the vacuum cleaner."""
        if self.vacuum_state is not None:
            try:
                return STATE_CODE_TO_STATE[int(self.vacuum_state)]
            except KeyError:
                _LOGGER.error(
                    "STATE_CODE not supported: %s",
                    self.vacuum_state,
                )
                return None

    @property
    def error(self):
        """Return the error of the vacuum cleaner."""
        if self.vacuum_error is not None:
            try:
                return ERROR_CODE_TO_ERROR.get(self.vacuum_error, "Unknown")
            except KeyError:
                _LOGGER.error(
                    "ERROR_CODE not supported: %s",
                    self.vacuum_error,
                )
                return None

    @property
    def battery_level(self):
        """Return the battery level of the vacuum cleaner."""
        if self.vacuum_state is not None:
            return self.battery_percentage

    @property
    def fan_speed(self):
        """Return the fan speed of the vacuum cleaner."""
        if self.vacuum_state is not None:
            speed = self._current_fan_speed
            if speed in self._fan_speeds_reverse:
                return SPEED_CODE_TO_NAME.get(self._current_fan_speed, "Unknown")

            _LOGGER.debug("Unable to find reverse for %s", speed)

            return speed

    @property
    def fan_speed_list(self):
        """Get the list of available fan speed steps of the vacuum cleaner."""
        return list(self._fan_speeds_reverse)

    @property
    def device_state_attributes(self):
        """Return the specific state attributes of this vacuum cleaner."""
        if self.vacuum_state is not None:
            return {
                ATTR_STATUS: STATE_CODE_TO_STATE[int(self.vacuum_state)],
                ATTR_ERROR:  ERROR_CODE_TO_ERROR.get(self.vacuum_error, "Unknown"),
              ATTR_FAN_SPEED: SPEED_CODE_TO_NAME.get(self._current_fan_speed, "Unknown"),
                ATTR_MAIN_BRUSH_LIFE_LEVEL: self._main_brush_life_level,
                ATTR_SIDE_BRUSH_LIFE_LEVEL: self._side_brush_life_level,
                ATTR_FILTER_LIFE_LEVEL: self._filter_life_level,
                ATTR_CLEANING_AREA: self._cleaning_area,
                ATTR_CLEANING_TIME: self._cleaning_time,              
            } 

    @property
    def supported_features(self):
        """Flag vacuum cleaner robot features that are supported."""
        return SUPPORT_XIAOMI

    async def _try_command(self, mask_error, func, *args, **kwargs):
        """Call a vacuum command handling error messages."""
        try:
            await self.hass.async_add_executor_job(partial(func, *args, **kwargs))
            return True
        except DeviceException as exc:
            _LOGGER.error(mask_error, exc)
            return False

    async def async_locate(self, **kwargs):
        """Locate the vacuum cleaner."""
        await self._try_command("Unable to locate the botvac: %s", self._vacuum.find)

    async def async_start(self):
        """Start or resume the cleaning task."""
        await self._try_command(
            "Unable to start the vacuum: %s", self._vacuum.start)

    async def async_stop(self, **kwargs):
        """Stop the vacuum cleaner."""
        await self._try_command("Unable to stop: %s", self._vacuum.stop)

    async def async_pause(self):
        """Pause the cleaning task."""
        await self._try_command("Unable to set start/pause: %s", self._vacuum.stop)

    async def async_return_to_base(self, **kwargs):
        """Set the vacuum cleaner to return to the dock."""
        await self._try_command("Unable to return home: %s", self._vacuum.return_home)

    async def async_set_fan_speed(self, fan_speed, **kwargs):
        """Set fan speed."""
        if fan_speed in self._fan_speeds_reverse:
            fan_speed = self._fan_speeds_reverse[fan_speed]
        else:
            try:
                fan_speed = int(fan_speed)
            except ValueError as exc:
                _LOGGER.error(
                    "Fan speed step not recognized (%s). Valid speeds are: %s",
                    exc,
                    self.fan_speed_list,
                )
                return
        await self._try_command(
            "Unable to set fan speed: %s", self._vacuum.set_fan_speed, fan_speed)    

    def update(self):
        """Fetch state from the device."""
        try:
            state = self._vacuum.status()
            self.vacuum_state = state.status
            self.vacuum_error = state.error

            self._fan_speeds = SPEED_CODE_TO_NAME
            self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()}

            self.battery_percentage = state.battery

            self._current_fan_speed = state.fan_speed

            self._main_brush_life_level = state.brush_life_level

            self._side_brush_life_level = state.side_brush_life_level

            self._filter_life_level = state.filter_life_level

            self._cleaning_area = state.clean_area
            self._cleaning_time = state.clean_time

        except OSError as exc:
            _LOGGER.error("Got OSError while fetching the state: %s", exc) 

Thank you very much!!

TaGGoU91 commented 3 years ago

Hello, I am coming here to see if you can help me starting the vacuum from the command line interface from a debian where the python module miio has been installed. I have the IP address, the token and the Device ID. The device ID has been obtained by using a little executable from my windows computer.

If i use the command : miiocli device --ip 192.168.98.56 --token 89QS7D897SQ8D789QS7D8SQ789D7QS info

I do have this answer : Model: mijia.vacuum.v2 Hardware version: esp32 Firmware version: 2.0.8 The model is well the Mop Essential - MJSTG1

I did try the following lines

miiocli device --ip 192.168.98.56 --token 89QS7D897SQ8D789QS7D8SQ789D7QS raw_command "method":"action","params":{"did":"56445231231","siid":2,"aiid":1}
miiocli vacuum --ip 192.168.98.56 --token 89QS7D897SQ8D789QS7D8SQ789D7QS raw_command "method":"action","params":{"did":"56445231231","siid":2,"aiid":1}

Gives me the following error :

Traceback (most recent call last):
  File "/usr/local/bin/miiocli", line 10, in <module>
    sys.exit(create_cli())
  File "/usr/local/lib/python3.7/dist-packages/miio/cli.py", line 63, in create_cli
    return cli(auto_envvar_prefix="MIIO")
  File "/usr/local/lib/python3.7/dist-packages/miio/click_common.py", line 59, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1259, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1257, in invoke
    sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 700, in make_context
    self.parse_args(ctx, args)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1048, in parse_args
    value, args = param.handle_parse_result(ctx, opts, args)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1623, in handle_parse_result
    value = self.full_process_value(ctx, value)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1589, in full_process_value
    value = self.process_value(ctx, value)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1579, in process_value
    return self.type_cast_value(ctx, value)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1568, in type_cast_value
    return _convert(value, (self.nargs != 1) + bool(self.multiple))
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1565, in _convert
    return self.type(value, self, ctx)
  File "/usr/local/lib/python3.7/dist-packages/click/types.py", line 46, in __call__
    return self.convert(value, param, ctx)
  File "/usr/local/lib/python3.7/dist-packages/miio/click_common.py", line 107, in convert
    return ast.literal_eval(value)
  File "/usr/lib/python3.7/ast.py", line 46, in literal_eval
    node_or_string = parse(node_or_string, mode='eval')
  File "/usr/lib/python3.7/ast.py", line 35, in parse
    return compile(source, filename, mode, PyCF_ONLY_AST)
  File "<unknown>", line 1
    method:action,params:siid:2
          ^
SyntaxError: invalid syntax

I also did try this lines :

miiocli device --ip 192.168.98.56 --token 89QS7D897SQ8D789QS7D8SQ789D7QS raw_command app_start
miiocli vacuum --ip 192.168.98.56 --token 89QS7D897SQ8D789QS7D8SQ789D7QS raw_command app_start
mirobo device --ip 192.168.98.56 --token 89QS7D897SQ8D789QS7D8SQ789D7QS raw_command app_start

That gives me the following error

Running command raw_command
Error: {'code': -9999, 'message': 'undefined command'}

So i do understand that the method:action seems to not be well written, but I did not succeed on finding the good way. I have read the documentation without success also.

Thanks if you can give me some help on this.

rytilahti commented 3 years ago

This should be supported now with #867 being merged so I'm closing this. @TaGGoU91 please try the current git master, miiocli g1vacuum should work without raw commands.

To execute custom actions, you cannot use mirobo nor miiocli device (but miiocli miotdevice should work):

$ miiocli miotdevice --ip 127.0.0.1 --token 12341234123412341234123412341234 call_action_by --help
WARNING:miio.miot_device:Neither the class nor the parameter defines the mapping
Usage: miiocli miotdevice call_action_by [OPTIONS] SIID AIID [PARAMS]

  Call an action.

where SIID and AIID are required (and params is the payload, if needed).

netique commented 2 years ago

Hi,

I have an issue getting a token:

miiocli discover                                                                
INFO:miio.miioprotocol:Sending discovery to <broadcast> with timeout of 5s..
INFO:miio.miioprotocol:  IP 192.168.0.248 (ID: 1d670515) - token: b'ffffffffffffffffffffffffffffffff'

The device is found on the correct IP address, but the token returned does not work, obviously. Any ideas?