-
-
Notifications
You must be signed in to change notification settings - Fork 569
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for Xiaomi Mijia G1 (mijia.vacuum.v2) (#867)
* 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
Showing
3 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,4 @@ __pycache__ | |
.coverage | ||
|
||
docs/_build/ | ||
.vscode/settings.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |