Source code for miio.integrations.mijia.vacuum.g1vacuum

import logging
from datetime import timedelta
from enum import Enum
from typing import Dict

import click

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

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

SUPPORTED_MODELS = [MIJIA_VACUUM_V1, MIJIA_VACUUM_V2]

MAPPING = {
    # 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},
}

MIOT_MAPPING = {model: MAPPING for model in SUPPORTED_MODELS}

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",
}


[docs] class G1ChargeState(Enum): """Charging Status.""" Discharging = 0 Charging = 1 FullyCharged = 2
[docs] class G1State(Enum): """Vacuum Status.""" Idle = 1 Sweeping = 2 Paused = 3 Error = 4 Charging = 5 GoCharging = 6
[docs] class G1Consumable(Enum): """Consumables.""" MainBrush = "main_brush_life_level" SideBrush = "side_brush_life_level" Filter = "filter_life_level"
[docs] class G1VacuumMode(Enum): """Vacuum Mode.""" GlobalClean = 1 SpotClean = 2 Wiping = 3
[docs] class G1WaterLevel(Enum): """Water Flow Level.""" Level1 = 1 Level2 = 2 Level3 = 3
[docs] class G1FanSpeed(Enum): """Fan speeds.""" Mute = 0 Standard = 1 Medium = 2 High = 3
[docs] class G1Languages(Enum): """Languages.""" Chinese = 0 English = 1
[docs] class G1MopState(Enum): """Mop Status.""" Off = 0 On = 1
[docs] class G1Status(DeviceStatus): """Container for status reports from Mijia Vacuum G1.""" def __init__(self, data): """Response (MIoT format) of a Mijia Vacuum G1 (mijia.vacuum.v2) Example:: [ {'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} ] """ 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"])
[docs] 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"])
[docs] class G1Vacuum(MiotDevice): """Support for G1 vacuum (G1, mijia.vacuum.v2).""" _mappings = MIOT_MAPPING
[docs] @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) } )
[docs] @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) } )
[docs] @command() def home(self): """Home.""" return self.call_action_from_mapping("home")
[docs] @command() def start(self) -> None: """Start Cleaning.""" return self.call_action_from_mapping("start")
[docs] @command() def stop(self): """Stop Cleaning.""" return self.call_action_from_mapping("stop")
[docs] @command() def find(self) -> None: """Find the robot.""" return self.call_action_from_mapping("find")
[docs] @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_from_mapping("reset_main_brush_life_level") elif consumable.name == G1Consumable.SideBrush: return self.call_action_from_mapping("reset_side_brush_life_level") elif consumable.name == G1Consumable.Filter: return self.call_action_from_mapping("reset_filter_life_level")
[docs] @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)
[docs] @command() def fan_speed_presets(self) -> Dict[str, int]: """Return available fan speed presets.""" return {x.name: x.value for x in G1FanSpeed}
[docs] @command(click.argument("speed", type=int)) def set_fan_speed_preset(self, speed_preset: int) -> None: """Set fan speed preset speed.""" if speed_preset not in self.fan_speed_presets().values(): raise ValueError( f"Invalid preset speed {speed_preset}, not in: {self.fan_speed_presets().values()}" ) return self.set_property("fan_speed", speed_preset)