Skip to content

Commit

Permalink
Support for Xiaomi Mijia G1 (mijia.vacuum.v2) (#867)
Browse files Browse the repository at this point in the history
* Create g1vacuum.py

* Update g1vacuum.py

* __init__py

forgot to commit that too

* Update g1vacuum.py

* Update g1vacuum.py

* Add files via upload

* Update __init__.py

Resolved little conflict

* Update __init__.py

* Update g1vacuum.py

* Update g1vacuum.py

* Update g1vacuum.py

* Update miio/g1vacuum.py

Co-authored-by: Teemu R. <tpr@iki.fi>

* Update miio/g1vacuum.py

Co-authored-by: Teemu R. <tpr@iki.fi>

* Update miio/g1vacuum.py

Co-authored-by: Teemu R. <tpr@iki.fi>

* Update g1vacuum.py

* Update g1vacuum.py

Cleaning Time now also timedelta, corrected a small glitch.

* Update README.rst

* Update README.rst

typo

* Fixed Linting Errors

* Delete settings.json

* Hope black won't error out again this time

* Update g1vacuum.py

*sigh

* changes by local pre-commit

now in vscode workflow

* Create test_g1vacuum.py

* Update test_g1vacuum.py

* Update test_g1vacuum.py

* Update test_g1vacuum.py

* Update test_g1vacuum.py

* Update .gitignore

* Delete test_g1vacuum.py

Test removed

Co-authored-by: Teemu R. <tpr@iki.fi>
  • Loading branch information
neturmel and rytilahti committed Sep 2, 2021
1 parent fc47803 commit 981368c
Show file tree
Hide file tree
Showing 3 changed files with 384 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ __pycache__
.coverage

docs/_build/
.vscode/settings.json
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Supported devices
- Xiaomi Mijia 360 1080p
- Xiaomi Mijia STYJ02YM (Viomi)
- Xiaomi Mijia 1C STYTJ01ZHM (Dreame)
- Xiaomi Mi Home (Mijia) G1 Robot Vacuum Mop MJSTG1
- Xiaomi Roidmi Eve
- Xiaomi Mi Smart WiFi Socket
- Xiaomi Chuangmi Plug V1 (1 Socket, 1 USB Port)
Expand Down
382 changes: 382 additions & 0 deletions miio/g1vacuum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,382 @@
import logging
from datetime import timedelta
from enum import Enum

import click

from .click_common import EnumType, command, format_output
from .miot_device import DeviceStatus, MiotDevice

_LOGGER = logging.getLogger(__name__)
MIJIA_VACUUM_V2 = "mijia.vacuum.v2"

MIOT_MAPPING = {
MIJIA_VACUUM_V2: {
# https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:vacuum:0000A006:mijia-v1:1
"battery": {"siid": 3, "piid": 1},
"charge_state": {"siid": 3, "piid": 2},
"error_code": {"siid": 2, "piid": 2},
"state": {"siid": 2, "piid": 1},
"fan_speed": {"siid": 2, "piid": 6},
"operating_mode": {"siid": 2, "piid": 4},
"mop_state": {"siid": 16, "piid": 1},
"water_level": {"siid": 2, "piid": 5},
"main_brush_life_level": {"siid": 14, "piid": 1},
"main_brush_time_left": {"siid": 14, "piid": 2},
"side_brush_life_level": {"siid": 15, "piid": 1},
"side_brush_time_left": {"siid": 15, "piid": 2},
"filter_life_level": {"siid": 11, "piid": 1},
"filter_time_left": {"siid": 11, "piid": 2},
"clean_area": {"siid": 9, "piid": 1},
"clean_time": {"siid": 9, "piid": 2},
# totals always return 0
"total_clean_area": {"siid": 9, "piid": 3},
"total_clean_time": {"siid": 9, "piid": 4},
"total_clean_count": {"siid": 9, "piid": 5},
"home": {"siid": 2, "aiid": 3},
"find": {"siid": 6, "aiid": 1},
"start": {"siid": 2, "aiid": 1},
"stop": {"siid": 2, "aiid": 2},
"reset_main_brush_life_level": {"siid": 14, "aiid": 1},
"reset_side_brush_life_level": {"siid": 15, "aiid": 1},
"reset_filter_life_level": {"siid": 11, "aiid": 1},
}
}

ERROR_CODES = {
0: "No error",
1: "Left Wheel stuck",
2: "Right Wheel stuck",
3: "Cliff error",
4: "Low battery",
5: "Bump error",
6: "Main Brush Error",
7: "Side Brush Error",
8: "Fan Motor Error",
9: "Dustbin Error",
10: "Charging Error",
11: "No Water Error",
12: "Pick Up Error",
}


class G1ChargeState(Enum):
"""Charging Status."""

Discharging = 0
Charging = 1
FullyCharged = 2


class G1State(Enum):
"""Vacuum Status."""

Idle = 1
Sweeping = 2
Paused = 3
Error = 4
Charging = 5
GoCharging = 6


class G1Consumable(Enum):
"""Consumables."""

MainBrush = "main_brush_life_level"
SideBrush = "side_brush_life_level"
Filter = "filter_life_level"


class G1VacuumMode(Enum):
"""Vacuum Mode."""

GlobalClean = 1
SpotClean = 2
Wiping = 3


class G1WaterLevel(Enum):
"""Water Flow Level."""

Level1 = 1
Level2 = 2
Level3 = 3


class G1FanSpeed(Enum):
"""Fan speeds."""

Mute = 0
Standard = 1
Medium = 2
High = 3


class G1Languages(Enum):
"""Languages."""

Chinese = 0
English = 1


class G1MopState(Enum):
"""Mop Status."""

Off = 0
On = 1


class G1Status(DeviceStatus):
"""Container for status reports from Mijia Vacuum G1."""

"""Response (MIoT format) of a Mijia Vacuum G1 (mijia.vacuum.v2)
[
{'did': 'battery', 'siid': 3, 'piid': 1, 'code': 0, 'value': 100},
{'did': 'charge_state', 'siid': 3, 'piid': 2, 'code': 0, 'value': 2},
{'did': 'error_code', 'siid': 2, 'piid': 2, 'code': 0, 'value': 0},
{'did': 'state', 'siid': 2, 'piid': 1, 'code': 0, 'value': 5},
{'did': 'fan_speed', 'siid': 2, 'piid': 6, 'code': 0, 'value': 1},
{'did': 'operating_mode', 'siid': 2, 'piid': 4, 'code': 0, 'value': 1},
{'did': 'mop_state', 'siid': 16, 'piid': 1, 'code': 0, 'value': 0},
{'did': 'water_level', 'siid': 2, 'piid': 5, 'code': 0, 'value': 2},
{'did': 'main_brush_life_level', 'siid': 14, 'piid': 1, 'code': 0, 'value': 99},
{'did': 'main_brush_time_left', 'siid': 14, 'piid': 2, 'code': 0, 'value': 17959}
{'did': 'side_brush_life_level', 'siid': 15, 'piid': 1, 'code': 0, 'value': 0 },
{'did': 'side_brush_time_left', 'siid': 15, 'piid': 2', 'code': 0, 'value': 0},
{'did': 'filter_life_level', 'siid': 11, 'piid': 1, 'code': 0, 'value': 99},
{'did': 'filter_time_left', 'siid': 11, 'piid': 2, 'code': 0, 'value': 8959},
{'did': 'clean_area', 'siid': 9, 'piid': 1, 'code': 0, 'value': 0},
{'did': 'clean_time', 'siid': 9, 'piid': 2, 'code': 0, 'value': 0}
]"""

def __init__(self, data):
self.data = data

@property
def battery(self) -> int:
"""Battery Level."""
return self.data["battery"]

@property
def charge_state(self) -> G1ChargeState:
"""Charging State."""
return G1ChargeState(self.data["charge_state"])

@property
def error_code(self) -> int:
"""Error code as returned by the device."""
return int(self.data["error_code"])

@property
def error(self) -> str:
"""Human readable error description, see also :func:`error_code`."""
try:
return ERROR_CODES[self.error_code]
except KeyError:
return "Definition missing for error %s" % self.error_code

@property
def state(self) -> G1State:
"""Vacuum Status."""
return G1State(self.data["state"])

@property
def fan_speed(self) -> G1FanSpeed:
"""Fan Speed."""
return G1FanSpeed(self.data["fan_speed"])

@property
def operating_mode(self) -> G1VacuumMode:
"""Operating Mode."""
return G1VacuumMode(self.data["operating_mode"])

@property
def mop_state(self) -> G1MopState:
"""Mop State."""
return G1MopState(self.data["mop_state"])

@property
def water_level(self) -> G1WaterLevel:
"""Water Level."""
return G1WaterLevel(self.data["water_level"])

@property
def main_brush_life_level(self) -> int:
"""Main Brush Life Level in %."""
return self.data["main_brush_life_level"]

@property
def main_brush_time_left(self) -> timedelta:
"""Main Brush Remaining Time in Minutes."""
return timedelta(minutes=self.data["main_brush_time_left"])

@property
def side_brush_life_level(self) -> int:
"""Side Brush Life Level in %."""
return self.data["side_brush_life_level"]

@property
def side_brush_time_left(self) -> timedelta:
"""Side Brush Remaining Time in Minutes."""
return timedelta(minutes=self.data["side_brush_time_left"])

@property
def filter_life_level(self) -> int:
"""Filter Life Level in %."""
return self.data["filter_life_level"]

@property
def filter_time_left(self) -> timedelta:
"""Filter remaining time."""
return timedelta(minutes=self.data["filter_time_left"])

@property
def clean_area(self) -> int:
"""Clean Area in cm2."""
return self.data["clean_area"]

@property
def clean_time(self) -> timedelta:
"""Clean time."""
return timedelta(minutes=self.data["clean_time"])


class G1CleaningSummary(DeviceStatus):
"""Container for cleaning summary from Mijia Vacuum G1."""

"""Response (MIoT format) of a Mijia Vacuum G1 (mijia.vacuum.v2)
[
{'did': 'total_clean_area', 'siid': 9, 'piid': 3, 'code': 0, 'value': 0},
{'did': 'total_clean_time', 'siid': 9, 'piid': 4, 'code': 0, 'value': 0},
{'did': 'total_clean_count', 'siid': 9, 'piid': 5, 'code': 0, 'value': 0}
]"""

def __init__(self, data) -> None:
self.data = data

@property
def total_clean_count(self) -> int:
"""Total Number of Cleanings."""
return self.data["total_clean_count"]

@property
def total_clean_area(self) -> int:
"""Total Area Cleaned in m2."""
return self.data["total_clean_area"]

@property
def total_clean_time(self) -> timedelta:
"""Total Cleaning Time."""
return timedelta(hours=self.data["total_clean_area"])


class G1Vacuum(MiotDevice):
"""Support for G1 vacuum (G1, mijia.vacuum.v2)."""

mapping = MIOT_MAPPING[MIJIA_VACUUM_V2]

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MIJIA_VACUUM_V2,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
self.model = model

@command(
default_output=format_output(
"",
"State: {result.state}\n"
"Error: {result.error}\n"
"Battery: {result.battery}%\n"
"Mode: {result.operating_mode}\n"
"Mop State: {result.mop_state}\n"
"Charge Status: {result.charge_state}\n"
"Fan speed: {result.fan_speed}\n"
"Water level: {result.water_level}\n"
"Main Brush Life Level: {result.main_brush_life_level}%\n"
"Main Brush Life Time: {result.main_brush_time_left}\n"
"Side Brush Life Level: {result.side_brush_life_level}%\n"
"Side Brush Life Time: {result.side_brush_time_left}\n"
"Filter Life Level: {result.filter_life_level}%\n"
"Filter Life Time: {result.filter_time_left}\n"
"Clean Area: {result.clean_area}\n"
"Clean Time: {result.clean_time}\n",
)
)
def status(self) -> G1Status:
"""Retrieve properties."""

return G1Status(
{
# max_properties limited to 10 to avoid "Checksum error"
# messages from the device.
prop["did"]: prop["value"] if prop["code"] == 0 else None
for prop in self.get_properties_for_mapping(max_properties=10)
}
)

@command(
default_output=format_output(
"",
"Total Cleaning Count: {result.total_clean_count}\n"
"Total Cleaning Time: {result.total_clean_time}\n"
"Total Cleaning Area: {result.total_clean_area}\n",
)
)
def cleaning_summary(self) -> G1CleaningSummary:
"""Retrieve properties."""

return G1CleaningSummary(
{
# max_properties limited to 10 to avoid "Checksum error"
# messages from the device.
prop["did"]: prop["value"] if prop["code"] == 0 else None
for prop in self.get_properties_for_mapping(max_properties=10)
}
)

@command()
def home(self):
"""Home."""
return self.call_action("home")

@command()
def start(self) -> None:
"""Start Cleaning."""
return self.call_action("start")

@command()
def stop(self):
"""Stop Cleaning."""
return self.call_action("stop")

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

@command(click.argument("consumable", type=G1Consumable))
def consumable_reset(self, consumable: G1Consumable):
"""Reset consumable information.
CONSUMABLE=main_brush_life_level|side_brush_life_level|filter_life_level
"""
if consumable.name == G1Consumable.MainBrush:
return self.call_action("reset_main_brush_life_level")
elif consumable.name == G1Consumable.SideBrush:
return self.call_action("reset_side_brush_life_level")
elif consumable.name == G1Consumable.Filter:
return self.call_action("reset_filter_life_level")

@command(
click.argument("fan_speed", type=EnumType(G1FanSpeed)),
default_output=format_output("Setting fan speed to {fan_speed}"),
)
def set_fan_speed(self, fan_speed: G1FanSpeed):
"""Set fan speed."""
return self.set_property("fan_speed", fan_speed.value)

0 comments on commit 981368c

Please sign in to comment.