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

Support for Xiaomi Mijia G1 (mijia.vacuum.v2) #867

Merged
merged 34 commits into from
Sep 2, 2021
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8bbad1a
Create g1vacuum.py
neturmel Nov 23, 2020
4048b56
Update g1vacuum.py
neturmel Nov 24, 2020
098ca3d
__init__py
neturmel Nov 26, 2020
f1cf345
Update g1vacuum.py
neturmel Nov 26, 2020
d4c0c0f
Update g1vacuum.py
neturmel Nov 26, 2020
ace52ca
Add files via upload
neturmel Jul 3, 2021
de626c2
Update __init__.py
neturmel Jul 3, 2021
0ea5c05
Update __init__.py
neturmel Jul 3, 2021
5b4eadf
Merge branch 'master' into mijia_vacuum_v2
neturmel Jul 6, 2021
a362fd1
Merge branch 'master' of https://github.com/rytilahti/python-miio int…
neturmel Jul 6, 2021
8d1928a
Update g1vacuum.py
neturmel Jul 6, 2021
d43b457
Update g1vacuum.py
neturmel Jul 8, 2021
742fc66
Update g1vacuum.py
neturmel Jul 8, 2021
5a49a23
Update miio/g1vacuum.py
neturmel Jul 9, 2021
55175bc
Update miio/g1vacuum.py
neturmel Jul 9, 2021
359ceba
Update miio/g1vacuum.py
neturmel Jul 9, 2021
87198b4
Update g1vacuum.py
neturmel Jul 9, 2021
9fdfcbf
Update g1vacuum.py
neturmel Jul 9, 2021
696b337
Update README.rst
neturmel Jul 10, 2021
4fe5b76
Update README.rst
neturmel Jul 10, 2021
d8572e3
Fixed Linting Errors
neturmel Jul 12, 2021
43d4e15
Merge branch 'mijia_vacuum_v2' of https://github.com/neturmel/python-…
neturmel Jul 12, 2021
d54a8b3
Delete settings.json
neturmel Jul 12, 2021
3e69570
Hope black won't error out again this time
neturmel Jul 12, 2021
a7bad96
Merge branch 'master' into mijia_vacuum_v2
neturmel Jul 13, 2021
617ae75
Update g1vacuum.py
neturmel Jul 13, 2021
8109ef7
changes by local pre-commit
neturmel Jul 13, 2021
60cad51
Create test_g1vacuum.py
neturmel Aug 3, 2021
1a7e707
Update test_g1vacuum.py
neturmel Aug 3, 2021
f58a4b9
Update test_g1vacuum.py
neturmel Aug 3, 2021
e1f653d
Update test_g1vacuum.py
neturmel Aug 11, 2021
a897001
Update test_g1vacuum.py
neturmel Aug 11, 2021
71c7fbf
Update .gitignore
neturmel Aug 11, 2021
5f9445d
Delete test_g1vacuum.py
neturmel Sep 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading