diff --git a/miio/integrations/dreame/vacuum/dreamevacuum_miot.py b/miio/integrations/dreame/vacuum/dreamevacuum_miot.py index ef1337c20..5cd29648b 100644 --- a/miio/integrations/dreame/vacuum/dreamevacuum_miot.py +++ b/miio/integrations/dreame/vacuum/dreamevacuum_miot.py @@ -26,6 +26,7 @@ DREAME_MOP_2 = "dreame.vacuum.p2150o" DREAME_TROUVER_FINDER = "dreame.vacuum.p2036" DREAME_D10_PLUS = "dreame.vacuum.r2205" +DREAME_X10_MOP = "dreame.vacuum.r2209" _DREAME_1C_MAPPING: MiotMapping = { # https://home.miot-spec.com/spec/dreame.vacuum.mc1808 @@ -164,6 +165,55 @@ "play_sound": {"siid": 7, "aiid": 2}, } +_DREAME_X10_MOP_MAPPING: MiotMapping = { + # https://home.miot-spec.com/spec/dreame.vacuum.r2209 + "battery_level": {"siid": 3, "piid": 1}, + "charging_state": {"siid": 3, "piid": 2}, + "device_fault": {"siid": 2, "piid": 2}, + "device_status": {"siid": 2, "piid": 1}, + "brush_left_time": {"siid": 9, "piid": 1}, + "brush_life_level": {"siid": 9, "piid": 2}, + "brush_left_time2": {"siid": 10, "piid": 1}, + "brush_life_level2": {"siid": 10, "piid": 2}, + "filter_life_level": {"siid": 11, "piid": 1}, + "filter_left_time": {"siid": 11, "piid": 2}, + "sensor_life_level": {"siid": 16, "piid": 1}, + "sensor_left_time": {"siid": 16, "piid": 2}, + "operating_mode": {"siid": 4, "piid": 1}, # work-mode + "cleaning_mode": {"siid": 4, "piid": 4}, + "delete_timer": {"siid": 8, "aiid": 1}, + "timer_enable": {"siid": 5, "piid": 1}, # do-not-disturb -> enable + "cleaning_time": {"siid": 4, "piid": 2}, + "cleaning_area": {"siid": 4, "piid": 3}, + "first_clean_time": {"siid": 12, "piid": 1}, + "total_clean_time": {"siid": 12, "piid": 2}, + "total_clean_times": {"siid": 12, "piid": 3}, + "total_clean_area": {"siid": 12, "piid": 4}, + "start_time": {"siid": 5, "piid": 2}, + "stop_time": {"siid": 5, "piid": 3}, # end-time + "map_view": {"siid": 6, "piid": 1}, # map-data + "frame_info": {"siid": 6, "piid": 2}, + "volume": {"siid": 7, "piid": 1}, + "voice_package": {"siid": 7, "piid": 2}, # voice-packet-id + "water_flow": {"siid": 4, "piid": 5}, # mop-mode + "water_box_carriage_status": {"siid": 4, "piid": 6}, # waterbox-status + "dust_auto_collect_status": {"siid": 15, "piid": 1}, # auto-collect-dust + "dust_collect_every": {"siid": 15, "piid": 2}, # Collect dust every n-th cleaing + "timezone": {"siid": 8, "piid": 1}, # time-zone + "home": {"siid": 3, "aiid": 1}, # start-charge + "locate": {"siid": 7, "aiid": 1}, # audio -> position + "start_clean": {"siid": 4, "aiid": 1}, + "stop_clean": {"siid": 4, "aiid": 2}, + "start_room_sweap": {"siid": 4, "aiid": 1}, + "reset_mainbrush_life": {"siid": 9, "aiid": 1}, + "reset_filter_life": {"siid": 11, "aiid": 1}, + "reset_sidebrush_life": {"siid": 10, "aiid": 1}, + "reset_sensor_life": {"siid": 16, "aiid": 1}, + "start_dust_collect": {"siid": 15, "aiid": 1}, + "move": {"siid": 21, "aiid": 1}, # not in documentation + "play_sound": {"siid": 7, "aiid": 2}, +} + MIOT_MAPPING: Dict[str, MiotMapping] = { DREAME_1C: _DREAME_1C_MAPPING, DREAME_F9: _DREAME_F9_MAPPING, @@ -176,6 +226,7 @@ DREAME_MOP_2: _DREAME_F9_MAPPING, DREAME_TROUVER_FINDER: _DREAME_TROUVER_FINDER_MAPPING, DREAME_D10_PLUS: _DREAME_TROUVER_FINDER_MAPPING, + DREAME_X10_MOP: _DREAME_X10_MOP_MAPPING, } @@ -213,6 +264,7 @@ class OperatingMode(FormattableEnum): ManualCleaning = 13 Sleeping = 14 ManualPaused = 17 + RoomCleaning = 18 ZonedCleaning = 19 @@ -237,6 +289,19 @@ class DeviceStatus(FormattableEnum): Upgrading = 14 +class DeviceStatusX10(FormattableEnum): + Sweeping = 1 + Idle = 2 + Paused = 3 + Error = 4 + GoCharging = 5 + Charging = 6 + SweepingAndMopping = 7 + Building = 11 + Mopping = 12 + ChargingComplete = 13 + + class WaterFlow(FormattableEnum): Low = 1 Medium = 2 @@ -247,6 +312,14 @@ def _enum_as_dict(cls): return {x.name: x.value for x in list(cls)} +def _get_device_status_enum_class(model): + """Return device status enum class for model""" + if model == DREAME_X10_MOP: + return DeviceStatusX10 + else: + return DeviceStatus + + def _get_cleaning_mode_enum_class(model): """Return cleaning mode enum class for model if found or None.""" if model == DREAME_1C: @@ -261,6 +334,7 @@ def _get_cleaning_mode_enum_class(model): DREAME_MOP_2_ULTRA, DREAME_MOP_2, DREAME_TROUVER_FINDER, + DREAME_X10_MOP, ): return CleaningModeDreameF9 return None @@ -333,6 +407,14 @@ def filter_left_time(self) -> str: def filter_life_level(self) -> str: return self.data["filter_life_level"] + @property + def sensor_left_time(self) -> str: + return self.data["sensor_left_time"] + + @property + def sensor_life_level(self) -> str: + return self.data["sensor_life_level"] + @property def device_fault(self) -> Optional[FaultStatus]: try: @@ -358,11 +440,17 @@ def operating_mode(self) -> Optional[OperatingMode]: return None @property - def device_status(self) -> Optional[DeviceStatus]: + def device_status(self): + device_status = self.data["device_status"] + device_status_enum_class = _get_device_status_enum_class(self.model) + + if not device_status_enum_class: + _LOGGER.error(f"Unknown model for device status ({self.model})") + return None try: - return DeviceStatus(self.data["device_status"]) - except TypeError: - _LOGGER.error("Unknown DeviceStatus (%s)", self.data["device_status"]) + return device_status_enum_class(device_status) + except ValueError: + _LOGGER.error(f"Unknown DeviceStatus ({device_status})") return None @property @@ -444,6 +532,7 @@ def life_brush_main(self) -> Optional[str]: return self.data.get("life_brush_main") # TODO: get/set water flow for Dreame 1C + @property def water_flow(self) -> Optional[WaterFlow]: try: @@ -464,6 +553,18 @@ def is_water_box_carriage_attached(self) -> Optional[bool]: return self.data["water_box_carriage_status"] == 1 return None + @property + def dust_auto_collect(self) -> Optional[bool]: + """Return True if auto dust collect is enabled, None if function is not present.""" + + if "dust_auto_collect_status" in self.data: + return self.data["dust_auto_collect_status"] == 1 + return None + + @property + def dust_collect_every(self) -> str: + return self.data["dust_collect_every"] + class DreameVacuum(MiotDevice): _mappings = MIOT_MAPPING @@ -480,6 +581,8 @@ class DreameVacuum(MiotDevice): "Device status: {result.device_status}\n" "Filter left level: {result.filter_left_time}\n" "Filter life level: {result.filter_life_level}\n" + "Sensor left level: {result.sensor_left_time}\n" + "Sensor life level: {result.sensor_life_level}\n" "Life brush main: {result.life_brush_main}\n" "Life brush side: {result.life_brush_side}\n" "Life sieve: {result.life_sieve}\n" @@ -495,6 +598,8 @@ class DreameVacuum(MiotDevice): "Volume: {result.volume}\n" "Water flow: {result.water_flow}\n" "Water box attached: {result.is_water_box_carriage_attached} \n" + "Dust auto collect: {result.dust_auto_collect}\n" + "Dust collect every n-th cleaning: {result.dust_collect_every} \n" "Cleaning time: {result.cleaning_time}\n" "Cleaning area: {result.cleaning_area}\n" "First clean time: {result.first_clean_time}\n" @@ -515,6 +620,7 @@ def status(self) -> DreameVacuumStatus: ) # TODO: check the actual limit for this + MANUAL_ROTATION_MAX = 120 MANUAL_ROTATION_MIN = -MANUAL_ROTATION_MAX MANUAL_DISTANCE_MAX = 300 @@ -555,6 +661,16 @@ def reset_sidebrush_life(self) -> None: """Reset side brush life.""" return self.call_action_from_mapping("reset_sidebrush_life") + @command() + def reset_sensor_life(self) -> None: + """Reset sensor life.""" + return self.call_action_from_mapping("reset_sensor_life") + + @command() + def start_dust_collect(self) -> None: + """Start dust collect""" + return self.call_action_from_mapping("start_dust_collect") + @command() def play_sound(self) -> None: """Play sound.""" @@ -692,6 +808,40 @@ def rotate(self, rotatation: int) -> None: ], ) + @command( + click.argument("room", default=3, type=int), + click.argument("clean_mode", default=1, type=int), + ) + def start_room_sweap(self, room: int, clean_mode: int) -> None: + """Start room cleaning. + + :param int room: ID of the room to be vacuumed + :param int clean_mode: Value of fan speed to be used for room cleaning + """ + + cleaningmode_enum = _get_cleaning_mode_enum_class(self.model) + cleaningmode = None + if not cleaningmode_enum: + return + try: + cleaningmode = cleaningmode_enum(clean_mode) + except ValueError: + _LOGGER.error(f"Unknown cleaning mode value passed {clean_mode}") + return None + self.call_action_from_mapping( + "start_room_sweap", + [ + { + "piid": 1, + "value": 18, + }, + { + "piid": 10, + "value": f"\u007b\u0022selects\u0022:[[{room},1,{cleaningmode.value},3,1]]\u007d", + }, + ], + ) + @command( click.argument("url", type=str), click.argument("md5sum", type=str, required=False), @@ -724,7 +874,6 @@ def set_voice(self, url: str, md5sum: str, size: int, voice_id: str): t = threading.Thread(target=server.serve_once) t.start() click.echo(f"Hosting file at {local_url}") - params = [ {"piid": 3, "value": voice_id}, {"piid": 4, "value": local_url}, @@ -734,5 +883,4 @@ def set_voice(self, url: str, md5sum: str, size: int, voice_id: str): result_status = self.call_action_from_mapping("set_voice", params=params) if result_status["code"] == 0: click.echo("Installation complete!") - return result_status