3561 lines
156 KiB
Python
3561 lines
156 KiB
Python
from __future__ import annotations
|
|
import logging
|
|
import time
|
|
import json
|
|
import re
|
|
import copy
|
|
import zlib
|
|
import base64
|
|
from datetime import datetime
|
|
from random import randrange
|
|
from threading import Timer
|
|
from typing import Any, Optional
|
|
|
|
from .types import (
|
|
PIID,
|
|
DIID,
|
|
DreameVacuumProperty,
|
|
DreameVacuumPropertyMapping,
|
|
DreameVacuumAction,
|
|
DreameVacuumActionMapping,
|
|
DreameVacuumChargingStatus,
|
|
DreameVacuumTaskStatus,
|
|
DreameVacuumState,
|
|
DreameVacuumWaterTank,
|
|
DreameVacuumCarpetSensitivity,
|
|
DreameVacuumStatus,
|
|
DreameVacuumErrorCode,
|
|
DreameVacuumRelocationStatus,
|
|
DreameVacuumDustCollection,
|
|
DreameVacuumAutoEmptyStatus,
|
|
DreameVacuumSelfWashBaseStatus,
|
|
DreameVacuumSuctionLevel,
|
|
DreameVacuumWaterVolume,
|
|
DreameVacuumMopPadHumidity,
|
|
DreameVacuumCleaningMode,
|
|
DreameVacuumSelfCleanArea,
|
|
DreameVacuumMopWashLevel,
|
|
DreameVacuumMoppingType,
|
|
CleaningHistory,
|
|
MapData,
|
|
Segment,
|
|
ATTR_ACTIVE_AREAS,
|
|
ATTR_ACTIVE_POINTS,
|
|
ATTR_ACTIVE_SEGMENTS
|
|
)
|
|
from .const import (
|
|
STATE_UNKNOWN,
|
|
STATE_UNAVAILABLE,
|
|
SUCTION_LEVEL_CODE_TO_NAME,
|
|
WATER_VOLUME_CODE_TO_NAME,
|
|
MOP_PAD_HUMIDITY_CODE_TO_NAME,
|
|
CLEANING_MODE_CODE_TO_NAME,
|
|
CARPET_SENSITIVITY_CODE_TO_NAME,
|
|
CHARGING_STATUS_CODE_TO_NAME,
|
|
RELOCATION_STATUS_CODE_TO_NAME,
|
|
SELF_WASH_BASE_STATUS_TO_NAME,
|
|
AUTO_EMPTY_STATUS_TO_NAME,
|
|
TASK_STATUS_CODE_TO_NAME,
|
|
STATE_CODE_TO_STATE,
|
|
ERROR_CODE_TO_ERROR_NAME,
|
|
ERROR_CODE_TO_ERROR_DESCRIPTION,
|
|
STATUS_CODE_TO_NAME,
|
|
WATER_TANK_CODE_TO_NAME,
|
|
DUST_COLLECTION_TO_NAME,
|
|
SELF_AREA_CLEAN_TO_NAME,
|
|
MOP_WASH_LEVEL_TO_NAME,
|
|
MOPPING_TYPE_TO_NAME,
|
|
ERROR_CODE_TO_IMAGE_INDEX,
|
|
PROPERTY_TO_NAME,
|
|
DEVICE_MAP_KEY,
|
|
AI_SETTING_SWITCH,
|
|
AI_SETTING_UPLOAD,
|
|
AI_SETTING_PET,
|
|
AI_SETTING_HUMAN,
|
|
AI_SETTING_FURNITURE,
|
|
AI_SETTING_FLUID,
|
|
ATTR_CHARGING,
|
|
ATTR_CLEANING_SEQUENCE,
|
|
ATTR_STARTED,
|
|
ATTR_PAUSED,
|
|
ATTR_RUNNING,
|
|
ATTR_RETURNING_PAUSED,
|
|
ATTR_RETURNING,
|
|
ATTR_MAPPING,
|
|
ATTR_ROOMS,
|
|
ATTR_CURRENT_SEGMENT,
|
|
ATTR_SELECTED_MAP,
|
|
ATTR_ID,
|
|
ATTR_NAME,
|
|
ATTR_ICON,
|
|
ATTR_STATUS,
|
|
ATTR_CLEANING_MODE,
|
|
ATTR_SUCTION_LEVEL,
|
|
ATTR_WATER_TANK,
|
|
ATTR_COMPLETED,
|
|
ATTR_CLEANING_TIME,
|
|
ATTR_CLEANED_AREA,
|
|
ATTR_MOP_PAD_HUMIDITY,
|
|
ATTR_MOP_PAD,
|
|
)
|
|
from .resources import ERROR_IMAGE
|
|
from .exceptions import (
|
|
DeviceUpdateFailedException,
|
|
InvalidActionException,
|
|
InvalidValueException,
|
|
)
|
|
from .protocol import DreameVacuumProtocol
|
|
from .map import DreameMapVacuumMapManager
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class DreameVacuumDevice:
|
|
"""Support for Dreame Vacuum"""
|
|
|
|
property_mapping: dict[DreameVacuumProperty,
|
|
dict[str, int]] = DreameVacuumPropertyMapping
|
|
action_mapping: dict[DreameVacuumAction,
|
|
dict[str, int]] = DreameVacuumActionMapping
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
host: str,
|
|
token: str,
|
|
mac: str = None,
|
|
username: str = None,
|
|
password: str = None,
|
|
country: str = None,
|
|
prefer_cloud: bool = False,
|
|
) -> None:
|
|
# Used for tracking the task status is changed from cleaning to completed
|
|
self.cleanup_completed: bool = False
|
|
# Used for easy filtering the device from cloud device list and generating unique ids
|
|
self.mac: str = None
|
|
self.token: str = None # Local api token
|
|
self.host: str = None # IP address or host name of the device
|
|
# Dictionary for storing the current property values
|
|
self.data: dict[DreameVacuumProperty, Any] = {}
|
|
self.available: bool = False # Last update is successful or not
|
|
|
|
self._update_running: bool = False # Update is running
|
|
# Previous cleaning mode for restoring it after water tank is installed or removed
|
|
self._previous_cleaning_mode: DreameVacuumCleaningMode = None
|
|
# Device do not request properties that returned -1 as result. This property used for overriding that behavior at first connection
|
|
self._ready: bool = False
|
|
# Last settings properties requested time
|
|
self._last_settings_request: float = 0
|
|
self._last_map_list_request: float = 0 # Last map list property requested time
|
|
self._last_map_request: float = 0 # Last map request trigger time
|
|
self._last_change: float = 0 # Last property change time
|
|
self._last_update_failed: float = 0 # Last update failed time
|
|
self._cleaning_history_update: float = 0 # Cleaning history update time
|
|
self._update_fail_count: int = 0 # Update failed counter
|
|
# Map Manager object. Only available when cloud connection is present
|
|
self._map_manager: DreameMapVacuumMapManager = None
|
|
self._update_callback = None # External update callback for device
|
|
self._error_callback = None # External update failed callback
|
|
# External update callbacks for specific device property
|
|
self._property_update_callback = {}
|
|
self._update_timer: Timer = None # Update schedule timer
|
|
# Used for requesting consumable properties after reset action otherwise they will only requested when cleaning completed
|
|
self._consumable_reset: bool = False
|
|
self._remote_control: bool = False
|
|
self._dirty_data: dict[DreameVacuumProperty, Any] = {}
|
|
|
|
self._name = name
|
|
self.mac = mac
|
|
self.token = token
|
|
self.host = host
|
|
self.two_factor_url = None
|
|
self.status = DreameVacuumDeviceStatus(self)
|
|
|
|
self.listen(self._task_status_changed,
|
|
DreameVacuumProperty.TASK_STATUS)
|
|
self.listen(self._status_changed,
|
|
DreameVacuumProperty.STATUS)
|
|
self.listen(
|
|
self._charging_status_changed, DreameVacuumProperty.CHARGING_STATUS
|
|
)
|
|
self.listen(self._water_tank_changed, DreameVacuumProperty.WATER_TANK)
|
|
self.listen(self._water_tank_changed, DreameVacuumProperty.AUTO_MOUNT_MOP)
|
|
self.listen(self._ai_obstacle_detection_changed,
|
|
DreameVacuumProperty.AI_DETECTION)
|
|
self.listen(self._auto_switch_settings_changed,
|
|
DreameVacuumProperty.AUTO_SWITCH_SETTINGS)
|
|
self.listen(self._intelligent_recognition_changed, DreameVacuumProperty.INTELLIGENT_RECOGNITION)
|
|
|
|
self._protocol = DreameVacuumProtocol(self.host, self.token, username, password, country, prefer_cloud)
|
|
if self._protocol.cloud:
|
|
self._map_manager = DreameMapVacuumMapManager(self._protocol)
|
|
|
|
self.listen(self._map_list_changed, DreameVacuumProperty.MAP_LIST)
|
|
self.listen(self._recovery_map_list_changed,
|
|
DreameVacuumProperty.RECOVERY_MAP_LIST)
|
|
self.listen(self._map_property_changed, DreameVacuumProperty.ERROR)
|
|
self.listen(
|
|
self._map_property_changed, DreameVacuumProperty.SELF_WASH_BASE_STATUS
|
|
)
|
|
self.listen(
|
|
self._map_property_changed, DreameVacuumProperty.CUSTOMIZED_CLEANING
|
|
)
|
|
|
|
self._map_manager.listen(self._property_changed)
|
|
self._map_manager.listen_error(self._update_failed)
|
|
|
|
def _request_properties(self, properties: list[DreameVacuumProperty] = None) -> bool:
|
|
"""Request properties from the device."""
|
|
if not properties:
|
|
properties = [prop for prop in DreameVacuumProperty]
|
|
|
|
# Remove write only and response only properties from default list
|
|
properties.remove(DreameVacuumProperty.SCHEDULE_ID)
|
|
properties.remove(DreameVacuumProperty.REMOTE_CONTROL)
|
|
properties.remove(DreameVacuumProperty.VOICE_CHANGE)
|
|
properties.remove(DreameVacuumProperty.VOICE_CHANGE_STATUS)
|
|
properties.remove(DreameVacuumProperty.MAP_RECOVERY)
|
|
properties.remove(DreameVacuumProperty.MAP_RECOVERY_STATUS)
|
|
properties.remove(DreameVacuumProperty.CLEANING_START_TIME)
|
|
properties.remove(DreameVacuumProperty.CLEAN_LOG_FILE_NAME)
|
|
properties.remove(DreameVacuumProperty.CLEANING_PROPERTIES)
|
|
properties.remove(DreameVacuumProperty.CLEAN_LOG_STATUS)
|
|
properties.remove(DreameVacuumProperty.MAP_DATA)
|
|
properties.remove(DreameVacuumProperty.FRAME_INFO)
|
|
properties.remove(DreameVacuumProperty.OBJECT_NAME)
|
|
properties.remove(DreameVacuumProperty.MAP_EXTEND_DATA)
|
|
properties.remove(DreameVacuumProperty.ROBOT_TIME)
|
|
properties.remove(DreameVacuumProperty.RESULT_CODE)
|
|
properties.remove(DreameVacuumProperty.OLD_MAP_DATA)
|
|
properties.remove(DreameVacuumProperty.WIFI_MAP)
|
|
properties.remove(DreameVacuumProperty.TAKE_PHOTO)
|
|
properties.remove(DreameVacuumProperty.CAMERA_LIGHT)
|
|
properties.remove(DreameVacuumProperty.CAMERA_BRIGHTNESS)
|
|
properties.remove(DreameVacuumProperty.STREAM_KEEP_ALIVE)
|
|
properties.remove(DreameVacuumProperty.STREAM_UPLOAD)
|
|
properties.remove(DreameVacuumProperty.STREAM_STATUS)
|
|
properties.remove(DreameVacuumProperty.STREAM_AUDIO)
|
|
properties.remove(DreameVacuumProperty.STREAM_RECORD)
|
|
properties.remove(DreameVacuumProperty.STREAM_CODE)
|
|
properties.remove(DreameVacuumProperty.STREAM_SET_CODE)
|
|
properties.remove(DreameVacuumProperty.STREAM_VERIFY_CODE)
|
|
properties.remove(DreameVacuumProperty.STREAM_RESET_CODE)
|
|
properties.remove(DreameVacuumProperty.STREAM_CRUISE_POINT)
|
|
properties.remove(DreameVacuumProperty.STREAM_PROPERTY)
|
|
properties.remove(DreameVacuumProperty.STREAM_FAULT)
|
|
properties.remove(DreameVacuumProperty.STREAM_TASK)
|
|
properties.remove(DreameVacuumProperty.STREAM_SPACE)
|
|
|
|
property_list = []
|
|
for prop in properties:
|
|
if prop in self.property_mapping:
|
|
mapping = self.property_mapping[prop]
|
|
# Do not include properties that are not exists on the device
|
|
if "aiid" not in mapping and (
|
|
not self._ready or prop.value in self.data
|
|
):
|
|
property_list.append({"did": str(prop.value), **mapping})
|
|
|
|
props = property_list.copy()
|
|
results = []
|
|
while props:
|
|
result = self._protocol.get_properties(props[:15])
|
|
if result is not None:
|
|
results.extend(result)
|
|
props[:] = props[15:]
|
|
|
|
changed = False
|
|
callbacks = []
|
|
for prop in results:
|
|
if prop["code"] == 0 and "value" in prop:
|
|
did = int(prop["did"])
|
|
value = prop["value"]
|
|
|
|
if did in self._dirty_data:
|
|
if self._dirty_data[did] != value:
|
|
_LOGGER.info("Property %s Value Discarded: %s <- %s", DreameVacuumProperty(did).name, self._dirty_data[did], value)
|
|
del self._dirty_data[did]
|
|
continue
|
|
|
|
if self.data.get(did, None) != value:
|
|
# Do not call external listener when map list and recovery map list properties changed
|
|
if did != DreameVacuumProperty.MAP_LIST.value and did != DreameVacuumProperty.RECOVERY_MAP_LIST.value:
|
|
changed = True
|
|
current_value = self.data.get(did)
|
|
if current_value is not None:
|
|
_LOGGER.info(
|
|
"Property %s Changed: %s -> %s", DreameVacuumProperty(did).name, current_value, value)
|
|
else:
|
|
_LOGGER.info(
|
|
"Property %s Added: %s", DreameVacuumProperty(did).name, value)
|
|
self.data[did] = value
|
|
if did in self._property_update_callback:
|
|
for callback in self._property_update_callback[did]:
|
|
callbacks.append([callback, current_value])
|
|
|
|
if not self._ready:
|
|
self.status.update_static_properties()
|
|
|
|
for callback in callbacks:
|
|
callback[0](callback[1])
|
|
|
|
if changed:
|
|
self._last_change = time.time()
|
|
if self._ready:
|
|
self._property_changed()
|
|
return changed
|
|
|
|
def _update_status(self, task_status: DreameVacuumTaskStatus, status: DreameVacuumStatus) -> None:
|
|
"""Update status properties on memory for map renderer to update the image before action is sent to the device."""
|
|
if task_status is not DreameVacuumTaskStatus.COMPLETED:
|
|
new_state = DreameVacuumState.SWEEPING
|
|
if self.status.cleaning_mode is DreameVacuumCleaningMode.MOPPING:
|
|
new_state = DreameVacuumState.MOPPING
|
|
elif self.status.cleaning_mode is DreameVacuumCleaningMode.SWEEPING_AND_MOPPING:
|
|
new_state = DreameVacuumState.SWEEPING_AND_MOPPING
|
|
self._update_property(
|
|
DreameVacuumProperty.STATE, new_state.value
|
|
)
|
|
|
|
if status is DreameVacuumStatus.STANDBY:
|
|
self._update_property(
|
|
DreameVacuumProperty.STATE, DreameVacuumState.IDLE.value
|
|
)
|
|
|
|
self._update_property(
|
|
DreameVacuumProperty.STATUS, status.value
|
|
)
|
|
self._update_property(
|
|
DreameVacuumProperty.TASK_STATUS, task_status.value
|
|
)
|
|
|
|
def _update_property(self, prop: DreameVacuumProperty, value: Any) -> Any:
|
|
"""Update device property on memory and notify listeners."""
|
|
if prop in self.property_mapping:
|
|
current_value = self.get_property(prop)
|
|
if current_value != value:
|
|
self._dirty_data[prop.value] = value
|
|
did = prop.value
|
|
self.data[did] = value
|
|
|
|
if did in self._property_update_callback:
|
|
for callback in self._property_update_callback[did]:
|
|
callback(current_value)
|
|
|
|
self._property_changed()
|
|
|
|
return current_value if current_value is not None else value
|
|
return None
|
|
|
|
def _map_property_changed(self, previous_property: Any = None) -> None:
|
|
"""Update last update time of the map when a property associated with rendering map changed."""
|
|
if self._map_manager:
|
|
self._map_manager.editor.refresh_map()
|
|
|
|
def _map_list_changed(self, previous_map_list: Any = None) -> None:
|
|
"""Update map list object name on map manager map list property when changed"""
|
|
if self._map_manager:
|
|
map_list = self.get_property(DreameVacuumProperty.MAP_LIST)
|
|
if map_list and map_list != "":
|
|
try:
|
|
map_list = json.loads(map_list)
|
|
object_name = map_list.get("object_name")
|
|
if object_name and object_name != "":
|
|
self._map_manager.set_map_list_object_name(map_list)
|
|
else:
|
|
self._last_map_list_request = 0
|
|
except:
|
|
pass
|
|
|
|
def _recovery_map_list_changed(self, previous_recovery_map_list: Any = None) -> None:
|
|
"""Update recovery list object name on map manager recovery list property when changed"""
|
|
if self._map_manager:
|
|
map_list = self.get_property(
|
|
DreameVacuumProperty.RECOVERY_MAP_LIST)
|
|
if map_list and map_list != "":
|
|
try:
|
|
map_list = json.loads(map_list)
|
|
object_name = map_list.get("object_name")
|
|
if object_name and object_name != "":
|
|
self._map_manager.set_recovery_map_list_object_name(
|
|
map_list)
|
|
else:
|
|
self._last_map_list_request = 0
|
|
except:
|
|
pass
|
|
|
|
def _water_tank_changed(self, previous_water_tank: Any = None) -> None:
|
|
"""Update cleaning mode on device when water tank status is changed."""
|
|
# App does not allow you to update cleaning mode when water tank or mop pad is not installed.
|
|
if self.get_property(DreameVacuumProperty.CLEANING_MODE) is not None:
|
|
new_list = CLEANING_MODE_CODE_TO_NAME.copy()
|
|
if not self.status.auto_mount:
|
|
if not self.status.water_tank_or_mop_installed:
|
|
new_list.pop(DreameVacuumCleaningMode.MOPPING)
|
|
new_list.pop(DreameVacuumCleaningMode.SWEEPING_AND_MOPPING)
|
|
if self.status.cleaning_mode != DreameVacuumCleaningMode.SWEEPING:
|
|
# Store current cleaning mode for future use when water tank is reinstalled
|
|
if not self.status.started:
|
|
self._previous_cleaning_mode = self.status.cleaning_mode
|
|
self.set_cleaning_mode(DreameVacuumCleaningMode.SWEEPING.value)
|
|
else:
|
|
if not self.status.mop_pad_lifting_available:
|
|
new_list.pop(DreameVacuumCleaningMode.SWEEPING)
|
|
|
|
if not self.status.started and self.status.sweeping:
|
|
if (
|
|
self._previous_cleaning_mode is not None
|
|
and self._previous_cleaning_mode
|
|
!= DreameVacuumCleaningMode.SWEEPING
|
|
):
|
|
self.set_cleaning_mode(self._previous_cleaning_mode.value)
|
|
else:
|
|
self.set_cleaning_mode(DreameVacuumCleaningMode.SWEEPING_AND_MOPPING.value)
|
|
# Store current cleaning mode for future use when water tank is removed
|
|
self._previous_cleaning_mode = self.status.cleaning_mode
|
|
|
|
self.status.cleaning_mode_list = {
|
|
v: k for k, v in new_list.items()}
|
|
|
|
def _task_status_changed(self, previous_task_status: Any = None) -> None:
|
|
"""Task status is a very important property and must be listened to trigger necessary actions when a task started or ended"""
|
|
if previous_task_status is not None:
|
|
if previous_task_status in DreameVacuumTaskStatus._value2member_map_:
|
|
previous_task_status = DreameVacuumTaskStatus(previous_task_status)
|
|
|
|
task_status = self.status.task_status
|
|
if previous_task_status is DreameVacuumTaskStatus.COMPLETED:
|
|
# as implemented on the app
|
|
self._update_property(DreameVacuumProperty.CLEANING_TIME, 0)
|
|
self._update_property(DreameVacuumProperty.CLEANED_AREA, 0)
|
|
|
|
if self._map_manager is not None:
|
|
# Update map data for renderer to update the map image according to the new task status
|
|
if previous_task_status is DreameVacuumTaskStatus.COMPLETED:
|
|
if (
|
|
task_status is DreameVacuumTaskStatus.AUTO_CLEANING
|
|
or task_status is DreameVacuumTaskStatus.ZONE_CLEANING
|
|
or task_status is DreameVacuumTaskStatus.SEGMENT_CLEANING
|
|
or task_status is DreameVacuumTaskStatus.SPOT_CLEANING
|
|
):
|
|
# Clear path on current map on cleaning start as implemented on the app
|
|
self._map_manager.editor.clear_path()
|
|
elif task_status is DreameVacuumTaskStatus.FAST_MAPPING:
|
|
# Clear current map on mapping start as implemented on the app
|
|
self._map_manager.editor.reset_map()
|
|
else:
|
|
self._map_manager.editor.refresh_map()
|
|
else:
|
|
self._map_manager.editor.refresh_map()
|
|
|
|
if task_status is DreameVacuumTaskStatus.COMPLETED:
|
|
if previous_task_status is DreameVacuumTaskStatus.FAST_MAPPING:
|
|
# as implemented on the app
|
|
self._update_property(
|
|
DreameVacuumProperty.CLEANING_TIME, 0)
|
|
self.cleanup_completed = False
|
|
if self._map_manager is not None:
|
|
# Mapping is completed, get the new map list from cloud
|
|
self._map_manager.request_next_map_list()
|
|
elif self.cleanup_completed is not None:
|
|
self.cleanup_completed = True
|
|
self._cleaning_history_update = time.time()
|
|
else:
|
|
self.cleanup_completed = None if self.status.fast_mapping else False
|
|
|
|
if (
|
|
task_status is DreameVacuumTaskStatus.COMPLETED
|
|
or previous_task_status is DreameVacuumTaskStatus.COMPLETED
|
|
):
|
|
# Get properties that only changes when task status is changed
|
|
properties = [
|
|
DreameVacuumProperty.MAIN_BRUSH_TIME_LEFT,
|
|
DreameVacuumProperty.MAIN_BRUSH_LEFT,
|
|
DreameVacuumProperty.SIDE_BRUSH_TIME_LEFT,
|
|
DreameVacuumProperty.SIDE_BRUSH_LEFT,
|
|
DreameVacuumProperty.FILTER_LEFT,
|
|
DreameVacuumProperty.FILTER_TIME_LEFT,
|
|
DreameVacuumProperty.SENSOR_DIRTY_LEFT,
|
|
DreameVacuumProperty.SENSOR_DIRTY_TIME_LEFT,
|
|
DreameVacuumProperty.SECONDARY_FILTER_LEFT,
|
|
DreameVacuumProperty.SECONDARY_FILTER_TIME_LEFT,
|
|
DreameVacuumProperty.MOP_PAD_LEFT,
|
|
DreameVacuumProperty.MOP_PAD_TIME_LEFT,
|
|
DreameVacuumProperty.SILVER_ION_TIME_LEFT,
|
|
DreameVacuumProperty.SILVER_ION_LEFT,
|
|
DreameVacuumProperty.DETERGENT_TIME_LEFT,
|
|
DreameVacuumProperty.DETERGENT_LEFT,
|
|
DreameVacuumProperty.TOTAL_CLEANING_TIME,
|
|
DreameVacuumProperty.CLEANING_COUNT,
|
|
DreameVacuumProperty.TOTAL_CLEANED_AREA,
|
|
DreameVacuumProperty.FIRST_CLEANING_DATE,
|
|
DreameVacuumProperty.SCHEDULE,
|
|
DreameVacuumProperty.SCHEDULE_CANCEL_REASON,
|
|
DreameVacuumProperty.CRUISE_SCHEDULE,
|
|
]
|
|
|
|
if self._map_manager is not None:
|
|
properties.extend(
|
|
[DreameVacuumProperty.MAP_LIST, DreameVacuumProperty.RECOVERY_MAP_LIST])
|
|
self._last_map_list_request = time.time()
|
|
|
|
try:
|
|
self._request_properties(properties)
|
|
except Exception as ex:
|
|
pass
|
|
|
|
def _status_changed(self, previous_status: Any = None) -> None:
|
|
if previous_status is not None:
|
|
if previous_status in DreameVacuumStatus._value2member_map_:
|
|
previous_status = DreameVacuumStatus(previous_status)
|
|
|
|
status = self.status.status
|
|
if self._remote_control and status is not DreameVacuumStatus.REMOTE_CONTROL and previous_status is not DreameVacuumStatus.REMOTE_CONTROL:
|
|
self._remote_control = False
|
|
|
|
if status is DreameVacuumStatus.CHARGING and previous_status is DreameVacuumStatus.BACK_HOME:
|
|
self._cleaning_history_update = time.time()
|
|
|
|
self._map_property_changed()
|
|
|
|
def _charging_status_changed(self, previous_charging_status: Any = None) -> None:
|
|
self._remote_control = False
|
|
self._map_property_changed()
|
|
|
|
def _ai_obstacle_detection_changed(self, previous_ai_obstacle_detection: Any = None) -> None:
|
|
"""AI Detection property returns multiple values as json or int this function parses and sets the sub properties to memory"""
|
|
value = self.get_property(DreameVacuumProperty.AI_DETECTION)
|
|
if isinstance(value, str):
|
|
settings = json.loads(value)
|
|
|
|
if AI_SETTING_SWITCH in settings:
|
|
self.status.ai_obstacle_detection = settings[AI_SETTING_SWITCH]
|
|
if AI_SETTING_UPLOAD in settings:
|
|
self.status.obstacle_image_upload = settings[AI_SETTING_UPLOAD]
|
|
if AI_SETTING_PET in settings:
|
|
self.status.pet_detection = settings[AI_SETTING_PET]
|
|
if AI_SETTING_HUMAN in settings:
|
|
self.status.human_detection = settings[AI_SETTING_HUMAN]
|
|
if AI_SETTING_FURNITURE in settings:
|
|
self.status.furniture_detection = settings[AI_SETTING_FURNITURE]
|
|
if AI_SETTING_FLUID in settings:
|
|
self.status.fluid_detection = settings[AI_SETTING_FLUID]
|
|
elif isinstance(value, int):
|
|
self.status.furniture_detection = (value & 1) == 1
|
|
self.status.ai_obstacle_detection = (value & 2) == 2
|
|
self.status.obstacle_picture = (value & 4) == 4
|
|
self.status.fluid_detection = (value & 8) == 8
|
|
self.status.pet_detection = (value & 16) == 16
|
|
self.status.obstacle_image_upload = (value & 32) == 32
|
|
self.status.ai_picture = (value & 64) == 64
|
|
|
|
self.status.ai_policy_accepted = bool(self.status.ai_policy_accepted or self.status.ai_obstacle_detection or self.status.obstacle_picture)
|
|
|
|
def _auto_switch_settings_changed(self, previous_auto_switch_settings: Any = None) -> None:
|
|
value = self.get_property(DreameVacuumProperty.AUTO_SWITCH_SETTINGS)
|
|
if isinstance(value, str) and len(value) > 2:
|
|
try:
|
|
settings = json.loads(value)
|
|
self.status.collision_avoidance = 0
|
|
self.status.fill_light = 0
|
|
self.status.auto_drying = 0
|
|
self.status.stain_avoidance = 0
|
|
self.status.mopping_type = 0
|
|
|
|
if len(settings):
|
|
for setting in settings:
|
|
key = setting["k"]
|
|
value = setting["v"]
|
|
|
|
if key == "LessColl":
|
|
self.status.collision_avoidance = value
|
|
elif key == "FillinLight":
|
|
self.status.fill_light = value
|
|
elif key == "AutoDry":
|
|
self.status.auto_drying = value
|
|
elif key == "StainIdentify":
|
|
self.status.stain_avoidance = value
|
|
elif key == "CleanType":
|
|
self.status.mopping_type = value
|
|
except:
|
|
pass
|
|
|
|
def _intelligent_recognition_changed(self, previous_intelligent_recognition: Any = None) -> None:
|
|
if not self.status.auto_switch_settings_available:
|
|
self.status.auto_drying = self.get_property(DreameVacuumProperty.INTELLIGENT_RECOGNITION)
|
|
|
|
def _request_cleaning_history(self) -> None:
|
|
"""Get and parse the cleaning history from cloud event data and set it to memory"""
|
|
if self.cloud_connected and self._cleaning_history_update != 0 and (self._cleaning_history_update == -1 or self.status._cleaning_history is None or (time.time() - self._cleaning_history_update >= 5 and self.status.task_status is DreameVacuumTaskStatus.COMPLETED)):
|
|
self._cleaning_history_update = 0
|
|
|
|
_LOGGER.debug("Get Cleaning History")
|
|
try:
|
|
# Limit the results
|
|
start = None
|
|
total = self.get_property(DreameVacuumProperty.CLEANING_COUNT)
|
|
if total > 0:
|
|
start = self.get_property(
|
|
DreameVacuumProperty.FIRST_CLEANING_DATE)
|
|
|
|
if start is None:
|
|
start = int(time.time())
|
|
if total is None:
|
|
total = 5
|
|
limit = 40
|
|
if total < 20:
|
|
limit = total + 20
|
|
|
|
# Cleaning history is generated from events of status property that has been sent to cloud by the device when it changed
|
|
result = self._protocol.cloud.get_device_event(
|
|
DIID(DreameVacuumProperty.STATUS,
|
|
self.property_mapping), limit, start
|
|
)
|
|
if result:
|
|
cleaning_history = []
|
|
history_size = 0
|
|
for data in result:
|
|
history_data = json.loads(data["value"])
|
|
|
|
history = CleaningHistory()
|
|
|
|
for history_data_item in history_data:
|
|
piid = history_data_item["piid"]
|
|
value = history_data_item["value"]
|
|
if piid == PIID(DreameVacuumProperty.STATUS, self.property_mapping):
|
|
if value in DreameVacuumStatus._value2member_map_:
|
|
history.status = DreameVacuumStatus(value)
|
|
else:
|
|
history.status = DreameVacuumStatus.UNKNOWN
|
|
elif piid == PIID(DreameVacuumProperty.CLEANING_TIME, self.property_mapping):
|
|
history.cleaning_time = value
|
|
elif piid == PIID(DreameVacuumProperty.CLEANED_AREA, self.property_mapping):
|
|
history.cleaned_area = value
|
|
elif piid == PIID(DreameVacuumProperty.SUCTION_LEVEL, self.property_mapping):
|
|
if value in DreameVacuumSuctionLevel._value2member_map_:
|
|
history.suction_level = DreameVacuumSuctionLevel(value)
|
|
else:
|
|
history.suction_level = DreameVacuumSuctionLevel.UNKNOWN
|
|
elif piid == PIID(DreameVacuumProperty.CLEANING_START_TIME, self.property_mapping):
|
|
history.date = datetime.fromtimestamp(value)
|
|
elif piid == PIID(DreameVacuumProperty.CLEAN_LOG_FILE_NAME, self.property_mapping):
|
|
history.file_name = value
|
|
elif piid == PIID(DreameVacuumProperty.CLEAN_LOG_STATUS, self.property_mapping):
|
|
history.completed = bool(value)
|
|
elif piid == PIID(DreameVacuumProperty.WATER_TANK, self.property_mapping):
|
|
if value in DreameVacuumWaterTank._value2member_map_:
|
|
history.water_tank = DreameVacuumWaterTank(
|
|
value)
|
|
else:
|
|
history.water_tank = DreameVacuumWaterTank.UNKNOWN
|
|
|
|
if history_size > 0 and cleaning_history[-1].date == history.date:
|
|
continue
|
|
|
|
cleaning_history.append(history)
|
|
history_size = history_size + 1
|
|
if history_size >= 20 or history_size >= total:
|
|
break
|
|
|
|
if self.status._cleaning_history != cleaning_history:
|
|
_LOGGER.debug("Cleaning History Changed")
|
|
self.status._cleaning_history = cleaning_history
|
|
if cleaning_history:
|
|
self.status._last_cleaning_time = cleaning_history[0].date.replace(tzinfo=datetime.now().astimezone().tzinfo)
|
|
|
|
if self._ready:
|
|
self._property_changed()
|
|
except:
|
|
_LOGGER.warning("Get Cleaning History failed!")
|
|
|
|
def _property_changed(self) -> None:
|
|
"""Call external listener when a property changed"""
|
|
if self._update_callback:
|
|
_LOGGER.debug("Update Callback")
|
|
self._update_callback()
|
|
|
|
def _update_failed(self, ex) -> None:
|
|
"""Call external listener when update failed"""
|
|
if self._error_callback:
|
|
self._error_callback(ex)
|
|
|
|
def _update_task(self) -> None:
|
|
"""Timer task for updating properties periodically"""
|
|
self._update_timer = None
|
|
|
|
try:
|
|
self.update()
|
|
self._update_fail_count = 0
|
|
except Exception as ex:
|
|
self._update_fail_count = self._update_fail_count + 1
|
|
if self.available:
|
|
self._last_update_failed = time.time()
|
|
if self._update_fail_count <= 3:
|
|
_LOGGER.warning("Update failed, retrying %s: %s", self._update_fail_count, ex)
|
|
else:
|
|
_LOGGER.debug("Update Failed: %s", ex)
|
|
self.available = False
|
|
self._update_failed(ex)
|
|
|
|
self.schedule_update(self._update_interval)
|
|
|
|
@staticmethod
|
|
def split_group_value(value: int, mop_pad_lifting_available: bool = False) -> list[int]:
|
|
if value is not None:
|
|
value_list = []
|
|
value_list.append((value & 3) if mop_pad_lifting_available else (value & 1))
|
|
byte1 = value >> 8
|
|
byte1 = byte1 & -769
|
|
value_list.append(byte1)
|
|
value_list.append(value >> 16)
|
|
return value_list
|
|
|
|
@staticmethod
|
|
def combine_group_value(values: list[int]) -> int:
|
|
if values and len(values) == 3:
|
|
num = 0
|
|
high = (num ^ values[2]) << 8
|
|
mid = (high ^ values[1]) << 8
|
|
low = mid ^ values[0]
|
|
return low
|
|
|
|
def connect_device(self) -> None:
|
|
"""Connect to the device api."""
|
|
_LOGGER.info("Connecting to device")
|
|
self.info = DreameVacuumDeviceInfo(self._protocol.connect())
|
|
if self.mac is None:
|
|
self.mac = self.info.mac_address
|
|
_LOGGER.info("Connected to device: %s %s", self.info.model, self.info.firmware_version)
|
|
|
|
self._last_settings_request = time.time()
|
|
self._last_map_list_request = self._last_settings_request
|
|
self._dirty_data = {}
|
|
self._request_properties()
|
|
self._last_update_failed = None
|
|
|
|
if self.device_connected and self._protocol.cloud is not None and (not self._ready or not self.available):
|
|
if self._map_manager:
|
|
model = self.info.model.split('.')
|
|
if len(model) == 3:
|
|
key = json.loads(zlib.decompress(base64.b64decode(DEVICE_MAP_KEY), zlib.MAX_WBITS | 32)).get(model[2])
|
|
if key:
|
|
self._map_manager.set_aes_iv(key)
|
|
|
|
if not self.status.lidar_navigation:
|
|
self._map_manager.set_vslam_map()
|
|
self._map_manager.set_update_interval(
|
|
self._map_update_interval)
|
|
self._map_manager.set_device_running(self.status.running, self.status.docked and not self.status.started)
|
|
|
|
if self.status.current_map is None:
|
|
self._map_manager.schedule_update(15)
|
|
self._map_manager.update()
|
|
self._last_map_request = self._last_settings_request
|
|
self._map_manager.schedule_update()
|
|
else:
|
|
self.update_map()
|
|
|
|
if self.cloud_connected:
|
|
self._cleaning_history_update = -1
|
|
self._request_cleaning_history()
|
|
|
|
if self.status.ai_detection_available and not self.status.ai_policy_accepted:
|
|
prop = "prop.s_ai_config"
|
|
response = self._protocol.cloud.get_batch_device_datas([prop])
|
|
if response and prop in response and response[prop]:
|
|
try:
|
|
self.status.ai_policy_accepted = json.loads(
|
|
response[prop]).get("privacyAuthed")
|
|
except:
|
|
pass
|
|
|
|
if not self.available:
|
|
self.available = True
|
|
if self._ready:
|
|
self._property_changed()
|
|
|
|
self._ready = True
|
|
|
|
def connect_cloud(self) -> None:
|
|
"""Connect to the cloud api."""
|
|
if self._protocol.cloud and not self._protocol.cloud.logged_in:
|
|
self._protocol.cloud.login()
|
|
if self._protocol.cloud.logged_in is False:
|
|
if self._protocol.cloud.two_factor_url:
|
|
self.two_factor_url = self._protocol.cloud.two_factor_url
|
|
self._property_changed()
|
|
self._map_manager.schedule_update(-1)
|
|
return
|
|
elif self._protocol.cloud.logged_in:
|
|
if self.two_factor_url:
|
|
self.two_factor_url = None
|
|
self._property_changed()
|
|
|
|
if self._protocol.connected:
|
|
self._map_manager.schedule_update(5)
|
|
self.token, self.host = self._protocol.cloud.get_info(
|
|
self.mac)
|
|
self._protocol.set_credentials(
|
|
self.host, self.token, self.mac)
|
|
|
|
def disconnect(self) -> None:
|
|
"""Disconnect from device and cancel timers"""
|
|
_LOGGER.info("Disconnect")
|
|
self.schedule_update(-1)
|
|
self._protocol.disconnect()
|
|
if self._map_manager:
|
|
self._map_manager.schedule_update(-1)
|
|
|
|
def listen(self, callback, property: DreameVacuumProperty = None) -> None:
|
|
"""Set callback functions for external listeners"""
|
|
if callback is None:
|
|
self._update_callback = None
|
|
self._property_update_callback = {}
|
|
return
|
|
|
|
if property is None:
|
|
self._update_callback = callback
|
|
else:
|
|
if property.value not in self._property_update_callback:
|
|
self._property_update_callback[property.value] = []
|
|
self._property_update_callback[property.value].append(callback)
|
|
|
|
def listen_error(self, callback) -> None:
|
|
"""Set error callback function for external listeners"""
|
|
self._error_callback = callback
|
|
|
|
def schedule_update(self, wait: float = None) -> None:
|
|
"""Schedule a device update for future"""
|
|
if not wait:
|
|
wait = self._update_interval
|
|
|
|
if self._update_timer is not None:
|
|
self._update_timer.cancel()
|
|
del self._update_timer
|
|
self._update_timer = None
|
|
|
|
if wait >= 0:
|
|
self._update_timer = Timer(wait, self._update_task)
|
|
self._update_timer.start()
|
|
|
|
def get_property(self, prop: DreameVacuumProperty) -> Any:
|
|
"""Get a device property from memory"""
|
|
if prop is not None and prop.value in self.data:
|
|
return self.data[prop.value]
|
|
return None
|
|
|
|
def set_property(self, prop: DreameVacuumProperty, value: Any) -> bool:
|
|
"""Sets property value using the existing property mapping and notify listeners
|
|
Property must be set on memory first and notify its listeners because device does not return new value immediately."""
|
|
|
|
self.schedule_update(10)
|
|
current_value = self._update_property(prop, value)
|
|
if current_value is not None:
|
|
self._last_change = time.time()
|
|
self._last_settings_request = 0
|
|
|
|
try:
|
|
mapping = self.property_mapping[prop]
|
|
result = self._protocol.set_property(mapping["siid"], mapping["piid"], value)
|
|
|
|
if result and result[0]["code"] != 0:
|
|
_LOGGER.error(
|
|
"Property not updated: %s: %s -> %s", prop, current_value, value
|
|
)
|
|
self._update_property(prop, current_value)
|
|
if prop.value in self._dirty_data:
|
|
del self._dirty_data[prop.value]
|
|
|
|
# Schedule the update for getting the updated property value from the device
|
|
# If property is actually updated nothing will happen otherwise it will return to previous value and notify its listeners. (Post optimistic approach)
|
|
self.schedule_update(2)
|
|
return True
|
|
except Exception as ex:
|
|
self._update_property(prop, current_value)
|
|
if prop.value in self._dirty_data:
|
|
del self._dirty_data[prop.value]
|
|
self.schedule_update(1)
|
|
raise DeviceUpdateFailedException(
|
|
"Set property failed %s: %s", prop.name, ex) from None
|
|
|
|
self.schedule_update(1)
|
|
return False
|
|
|
|
def get_map_for_render(self, map_index: int) -> MapData | None:
|
|
"""Makes changes on map data for device related properties for renderer.
|
|
Map manager does not need any device property for parsing and storing map data but map renderer does.
|
|
For example if device is running but not mopping renderer does not show no mopping areas and this function handles that so renderer does not need device data too."""
|
|
|
|
map_data = self.get_map(map_index)
|
|
if map_data:
|
|
if map_data.need_optimization:
|
|
map_data = self._map_manager.optimizer.optimize(map_data, self._map_manager.selected_map if map_data.saved_map_status == 2 else None)
|
|
map_data.need_optimization = False
|
|
|
|
map_data = copy.deepcopy(map_data)
|
|
|
|
if map_data.optimized_pixel_type is not None:
|
|
map_data.pixel_type = map_data.optimized_pixel_type
|
|
map_data.dimensions = map_data.optimized_dimensions
|
|
if map_data.optimized_charger_position is not None:
|
|
map_data.charger_position = map_data.optimized_charger_position
|
|
|
|
if (
|
|
self.status.started and not self.status.zone_cleaning
|
|
) or map_data.saved_map:
|
|
# Map data always contains last active areas
|
|
map_data.active_areas = None
|
|
|
|
if (
|
|
self.status.started and not self.status.spot_cleaning
|
|
) or map_data.saved_map:
|
|
# Map data always contains last active areas
|
|
map_data.active_points = None
|
|
|
|
if not self.status.segment_cleaning or map_data.saved_map:
|
|
# Map data always contains last active segments
|
|
map_data.active_segments = None
|
|
|
|
if not map_data.saved_map:
|
|
if self.status.started and self.status.sweeping:
|
|
# App does not render no mopping areas when cleaning mode is sweeping
|
|
map_data.no_mopping_areas = None
|
|
|
|
if (self.status.zone_cleaning and map_data.active_areas) or (self.status.spot_cleaning and map_data.active_points):
|
|
# App does not render segments when zone or spot cleaning
|
|
map_data.segments = None
|
|
else:
|
|
map_data.path = None
|
|
|
|
if not self.status.customized_cleaning or map_data.saved_map:
|
|
# App does not render customized cleaning settings on saved map list
|
|
map_data.cleanset = None
|
|
|
|
# Device currently may not be docked but map data can be old and still showing when robot is docked
|
|
map_data.docked = bool(map_data.docked or self.status.docked)
|
|
|
|
if not map_data.saved_map and not self.status.lidar_navigation and map_data.saved_map_status == 1 and map_data.docked:
|
|
# For correct scaling of vslam saved map
|
|
map_data.saved_map_status = 2
|
|
|
|
if map_data.charger_position == None and map_data.docked and map_data.robot_position:
|
|
map_data.charger_position = copy.deepcopy(map_data.robot_position)
|
|
if self.status.robot_shape != 2:
|
|
map_data.charger_position.a = map_data.robot_position.a + 180
|
|
|
|
if map_data.saved_map:
|
|
# App does not render robot position on saved map list
|
|
map_data.robot_position = None
|
|
|
|
# App does not render restricted zones on saved map list
|
|
map_data.walls = None
|
|
map_data.no_go_areas = None
|
|
map_data.no_mopping_areas = None
|
|
map_data.obstacles = None
|
|
|
|
elif map_data.charger_position and map_data.docked:
|
|
if not map_data.robot_position:
|
|
map_data.robot_position = copy.deepcopy(
|
|
map_data.charger_position)
|
|
|
|
return map_data
|
|
|
|
def get_map(self, map_index: int) -> MapData | None:
|
|
"""Get stored map data by index from map manager."""
|
|
if self._map_manager:
|
|
if self.status.multi_map:
|
|
return self._map_manager.get_map(map_index)
|
|
if map_index == 1:
|
|
return self._map_manager.selected_map
|
|
if map_index == 0:
|
|
return self.status.current_map
|
|
|
|
def update_map(self) -> None:
|
|
"""Trigger a map update.
|
|
This function is used for requesting map data when a image request has been made to renderer"""
|
|
|
|
if self._map_manager:
|
|
now = time.time()
|
|
if now - self._last_map_request > 120:
|
|
self._last_map_request = now
|
|
self._map_manager.set_update_interval(
|
|
self._map_update_interval)
|
|
self._map_manager.schedule_update(0.01)
|
|
|
|
def update(self) -> None:
|
|
"""Get properties from the device."""
|
|
_LOGGER.debug("Device update: %s", self._update_interval)
|
|
|
|
if self._update_running:
|
|
return
|
|
|
|
if not self.cloud_connected:
|
|
self.connect_cloud()
|
|
|
|
if not self.device_connected:
|
|
self.connect_device()
|
|
|
|
if not self.device_connected:
|
|
raise DeviceUpdateFailedException("Device cannot be reached")
|
|
|
|
self._update_running = True
|
|
|
|
# Read-only properties
|
|
properties = [
|
|
DreameVacuumProperty.STATE,
|
|
DreameVacuumProperty.ERROR,
|
|
DreameVacuumProperty.BATTERY_LEVEL,
|
|
DreameVacuumProperty.CHARGING_STATUS,
|
|
DreameVacuumProperty.STATUS,
|
|
DreameVacuumProperty.WATER_TANK,
|
|
DreameVacuumProperty.TASK_STATUS,
|
|
DreameVacuumProperty.WARN_STATUS,
|
|
DreameVacuumProperty.RELOCATION_STATUS,
|
|
DreameVacuumProperty.SELF_WASH_BASE_STATUS,
|
|
DreameVacuumProperty.DUST_COLLECTION,
|
|
DreameVacuumProperty.AUTO_EMPTY_STATUS,
|
|
DreameVacuumProperty.CLEANING_PAUSED,
|
|
DreameVacuumProperty.CLEANING_CANCEL,
|
|
DreameVacuumProperty.SCHEDULED_CLEAN,
|
|
DreameVacuumProperty.MOP_IN_STATION,
|
|
DreameVacuumProperty.MOP_PAD_INSTALLED,
|
|
DreameVacuumProperty.NO_WATER_WARNING,
|
|
#DreameVacuumProperty.SAVE_WATER_TIPS,
|
|
]
|
|
|
|
now = time.time()
|
|
if self.status.active:
|
|
# Only changed when robot is active
|
|
properties.extend(
|
|
[DreameVacuumProperty.CLEANED_AREA,
|
|
DreameVacuumProperty.CLEANING_TIME]
|
|
)
|
|
|
|
if self._consumable_reset:
|
|
# Consumable properties
|
|
properties.extend(
|
|
[
|
|
DreameVacuumProperty.MAIN_BRUSH_TIME_LEFT,
|
|
DreameVacuumProperty.MAIN_BRUSH_LEFT,
|
|
DreameVacuumProperty.SIDE_BRUSH_TIME_LEFT,
|
|
DreameVacuumProperty.SIDE_BRUSH_LEFT,
|
|
DreameVacuumProperty.FILTER_LEFT,
|
|
DreameVacuumProperty.FILTER_TIME_LEFT,
|
|
DreameVacuumProperty.SENSOR_DIRTY_LEFT,
|
|
DreameVacuumProperty.SENSOR_DIRTY_TIME_LEFT,
|
|
DreameVacuumProperty.MOP_PAD_LEFT,
|
|
DreameVacuumProperty.MOP_PAD_TIME_LEFT,
|
|
]
|
|
)
|
|
|
|
if now - self._last_settings_request > 9.5:
|
|
self._last_settings_request = now
|
|
|
|
# Read/Write properties
|
|
properties.extend(
|
|
[
|
|
DreameVacuumProperty.SUCTION_LEVEL,
|
|
DreameVacuumProperty.RESUME_CLEANING,
|
|
DreameVacuumProperty.CARPET_BOOST,
|
|
DreameVacuumProperty.MOP_CLEANING_REMAINDER,
|
|
DreameVacuumProperty.OBSTACLE_AVOIDANCE,
|
|
DreameVacuumProperty.AI_DETECTION,
|
|
DreameVacuumProperty.DRYING_TIME,
|
|
DreameVacuumProperty.AUTO_ADD_DETERGENT,
|
|
DreameVacuumProperty.CARPET_AVOIDANCE,
|
|
DreameVacuumProperty.CLEANING_MODE,
|
|
DreameVacuumProperty.WATER_ELECTROLYSIS,
|
|
DreameVacuumProperty.INTELLIGENT_RECOGNITION,
|
|
DreameVacuumProperty.AUTO_WATER_REFILLING,
|
|
DreameVacuumProperty.AUTO_MOUNT_MOP,
|
|
DreameVacuumProperty.MOP_WASH_LEVEL,
|
|
DreameVacuumProperty.CUSTOMIZED_CLEANING,
|
|
DreameVacuumProperty.CHILD_LOCK,
|
|
DreameVacuumProperty.CARPET_SENSITIVITY,
|
|
DreameVacuumProperty.TIGHT_MOPPING,
|
|
DreameVacuumProperty.CARPET_RECOGNITION,
|
|
DreameVacuumProperty.SELF_CLEAN,
|
|
DreameVacuumProperty.DND,
|
|
DreameVacuumProperty.DND_START,
|
|
DreameVacuumProperty.DND_END,
|
|
DreameVacuumProperty.DND_TASK,
|
|
DreameVacuumProperty.MULTI_FLOOR_MAP,
|
|
DreameVacuumProperty.VOLUME,
|
|
DreameVacuumProperty.AUTO_DUST_COLLECTING,
|
|
DreameVacuumProperty.AUTO_EMPTY_FREQUENCY,
|
|
DreameVacuumProperty.VOICE_PACKET_ID,
|
|
DreameVacuumProperty.TIMEZONE,
|
|
DreameVacuumProperty.MAP_SAVING,
|
|
DreameVacuumProperty.AUTO_SWITCH_SETTINGS,
|
|
DreameVacuumProperty.QUICK_COMMAND,
|
|
]
|
|
)
|
|
|
|
if not self.status.self_wash_base_available:
|
|
properties.append(DreameVacuumProperty.WATER_VOLUME)
|
|
|
|
if self._map_manager and not self.status.running and now - self._last_map_list_request > 60:
|
|
properties.extend([DreameVacuumProperty.MAP_LIST,
|
|
DreameVacuumProperty.RECOVERY_MAP_LIST])
|
|
self._last_map_list_request = time.time()
|
|
|
|
try:
|
|
self._request_properties(properties)
|
|
except Exception as ex:
|
|
self._update_running = False
|
|
raise DeviceUpdateFailedException(ex) from None
|
|
|
|
if self._consumable_reset:
|
|
self._consumable_reset = False
|
|
|
|
if self._map_manager:
|
|
self._map_manager.set_update_interval(self._map_update_interval)
|
|
self._map_manager.set_device_running(self.status.running, self.status.docked and not self.status.started)
|
|
|
|
if self.cloud_connected:
|
|
self._request_cleaning_history()
|
|
|
|
self._update_running = False
|
|
|
|
def call_action(self, action: DreameVacuumAction, parameters: dict[str, Any] = None) -> dict[str, Any] | None:
|
|
"""Call an action."""
|
|
if action not in self.action_mapping:
|
|
raise InvalidActionException(
|
|
f"Unable to find {action} in the action mapping"
|
|
)
|
|
|
|
mapping = self.action_mapping[action]
|
|
if "siid" not in mapping or "aiid" not in mapping:
|
|
raise InvalidActionException(
|
|
f"{action} is not an action (missing siid or aiid)"
|
|
)
|
|
|
|
if (
|
|
action is not DreameVacuumAction.REQUEST_MAP
|
|
and action is not DreameVacuumAction.UPDATE_MAP_DATA
|
|
):
|
|
self.schedule_update(10)
|
|
|
|
# Reset consumable on memory
|
|
if action is DreameVacuumAction.RESET_MAIN_BRUSH:
|
|
self._consumable_reset = True
|
|
self._update_property(DreameVacuumProperty.MAIN_BRUSH_LEFT, 100)
|
|
elif action is DreameVacuumAction.RESET_SIDE_BRUSH:
|
|
self._consumable_reset = True
|
|
self._update_property(DreameVacuumProperty.SIDE_BRUSH_LEFT, 100)
|
|
elif action is DreameVacuumAction.RESET_FILTER:
|
|
self._consumable_reset = True
|
|
self._update_property(DreameVacuumProperty.FILTER_LEFT, 100)
|
|
elif action is DreameVacuumAction.RESET_SENSOR:
|
|
self._consumable_reset = True
|
|
self._update_property(DreameVacuumProperty.SENSOR_DIRTY_LEFT, 100)
|
|
elif action is DreameVacuumAction.RESET_SECONDARY_FILTER:
|
|
self._consumable_reset = True
|
|
self._update_property(DreameVacuumProperty.SECONDARY_FILTER_LEFT, 100)
|
|
elif action is DreameVacuumAction.RESET_MOP_PAD:
|
|
self._consumable_reset = True
|
|
self._update_property(DreameVacuumProperty.MOP_PAD_LEFT, 100)
|
|
elif action is DreameVacuumAction.RESET_SILVER_ION:
|
|
self._consumable_reset = True
|
|
self._update_property(DreameVacuumProperty.SILVER_ION_LEFT, 100)
|
|
elif action is DreameVacuumAction.RESET_DETERGENT:
|
|
self._consumable_reset = True
|
|
self._update_property(DreameVacuumProperty.DETERGENT_LEFT, 100)
|
|
|
|
# Update listeners
|
|
if (
|
|
action is DreameVacuumAction.START or
|
|
action is DreameVacuumAction.START_CUSTOM or
|
|
action is DreameVacuumAction.STOP or
|
|
action is DreameVacuumAction.CHARGE or
|
|
action is DreameVacuumAction.UPDATE_MAP_DATA or
|
|
self._consumable_reset
|
|
):
|
|
self._property_changed()
|
|
|
|
try:
|
|
result = self._protocol.action(
|
|
mapping["siid"], mapping["aiid"], parameters)
|
|
if result and result.get("code") != 0:
|
|
result = None
|
|
except Exception as ex:
|
|
_LOGGER.error("Send action failed %s: %s", action.name, ex)
|
|
self.schedule_update(1)
|
|
return
|
|
|
|
if result:
|
|
_LOGGER.info("Send action %s", action.name)
|
|
self._last_change = time.time()
|
|
if (
|
|
action is not DreameVacuumAction.REQUEST_MAP
|
|
and action is not DreameVacuumAction.UPDATE_MAP_DATA
|
|
):
|
|
self._last_settings_request = 0
|
|
|
|
# Schedule update for retrieving new properties after action sent
|
|
self.schedule_update(3)
|
|
return result
|
|
|
|
def send_command(self, command: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Send a raw command to the device. This is mostly useful when trying out
|
|
commands which are not implemented by a given device instance. (Not likely)"""
|
|
|
|
if command == "" or parameters is None:
|
|
raise InvalidActionException("Invalid Command: (%s).", command)
|
|
|
|
self.schedule_update(10)
|
|
self._protocol.send(command, parameters, 1)
|
|
self.schedule_update(2)
|
|
|
|
def set_suction_level(self, suction_level: int) -> bool:
|
|
"""Set suction level."""
|
|
if self.status.started and (self.status.customized_cleaning and not (self.status.zone_cleaning or self.status.spot_cleaning)):
|
|
raise InvalidActionException(
|
|
"Cannot set suction level when customized cleaning is enabled"
|
|
)
|
|
return self.set_property(DreameVacuumProperty.SUCTION_LEVEL, int(suction_level))
|
|
|
|
def set_cleaning_mode(self, cleaning_mode: int) -> bool:
|
|
"""Set cleaning mode."""
|
|
if self.status.started:
|
|
raise InvalidActionException(
|
|
"Cannot set cleaning mode while vacuum is running"
|
|
)
|
|
|
|
if not self.status.auto_mount:
|
|
if cleaning_mode is DreameVacuumCleaningMode.SWEEPING.value:
|
|
if self.status.water_tank_or_mop_installed and not self.status.mop_pad_lifting_available:
|
|
if self.status.self_wash_base_available:
|
|
raise InvalidActionException(
|
|
"Cannot set sweeping while mop pads are installed"
|
|
)
|
|
else:
|
|
raise InvalidActionException(
|
|
"Cannot set sweeping while water tank is installed"
|
|
)
|
|
elif not self.status.water_tank_or_mop_installed:
|
|
if self.status.self_wash_base_available:
|
|
raise InvalidActionException(
|
|
"Cannot set mopping while mop pads are not installed"
|
|
)
|
|
else:
|
|
raise InvalidActionException(
|
|
"Cannot set mopping while water tank is not installed"
|
|
)
|
|
|
|
if self.status.self_wash_base_available:
|
|
values = DreameVacuumDevice.split_group_value(self.get_property(DreameVacuumProperty.CLEANING_MODE), self.status.mop_pad_lifting_available)
|
|
if values and len(values) == 3:
|
|
if self.status.mop_pad_lifting_available:
|
|
if cleaning_mode == 2:
|
|
values[0] = 0
|
|
elif cleaning_mode == 0:
|
|
values[0] = 2
|
|
else:
|
|
values[0] = cleaning_mode
|
|
elif cleaning_mode == 2:
|
|
values[0] = 0
|
|
|
|
cleaning_mode = DreameVacuumDevice.combine_group_value(values)
|
|
elif self.status.mop_pad_lifting_available:
|
|
if cleaning_mode == 2:
|
|
cleaning_mode = 0
|
|
elif cleaning_mode == 0:
|
|
cleaning_mode = 2
|
|
|
|
return self.set_property(DreameVacuumProperty.CLEANING_MODE, int(cleaning_mode))
|
|
|
|
def set_mop_pad_humidity(self, mop_pad_humidity: int) -> bool:
|
|
"""Set mop pad humidity."""
|
|
if self.status.self_wash_base_available:
|
|
if self.status.started and (self.status.customized_cleaning and not (self.status.zone_cleaning or self.status.spot_cleaning)):
|
|
raise InvalidActionException(
|
|
"Cannot set mop pad humidity when customized cleaning is enabled"
|
|
)
|
|
|
|
values = DreameVacuumDevice.split_group_value(self.get_property(DreameVacuumProperty.CLEANING_MODE), self.status.mop_pad_lifting_available)
|
|
if values and len(values) == 3:
|
|
values[2] = mop_pad_humidity
|
|
return self.set_property(DreameVacuumProperty.CLEANING_MODE, DreameVacuumDevice.combine_group_value(values))
|
|
|
|
def set_water_volume(self, water_volume: int) -> bool:
|
|
"""Set water volume."""
|
|
if not self.status.self_wash_base_available:
|
|
if self.status.started and (self.status.customized_cleaning and not (self.status.zone_cleaning or self.status.spot_cleaning)):
|
|
raise InvalidActionException(
|
|
"Cannot set water volume when customized cleaning is enabled"
|
|
)
|
|
|
|
return self.set_property(DreameVacuumProperty.WATER_VOLUME, int(water_volume))
|
|
|
|
def set_dnd_enabled(self, dnd_enabled: bool) -> bool:
|
|
"""Set do not disturb function"""
|
|
return self.set_property(DreameVacuumProperty.DND, bool(dnd_enabled))
|
|
|
|
def set_dnd_start(self, dnd_start: str) -> bool:
|
|
"""Set do not disturb function"""
|
|
time_pattern = re.compile("([0-1][0-9]|2[0-3]):[0-5][0-9]$")
|
|
if not re.match(time_pattern, dnd_start):
|
|
raise InvalidValueException(
|
|
"DND start time is not valid: (%s).", dnd_start)
|
|
return self.set_property(DreameVacuumProperty.DND_START, dnd_start)
|
|
|
|
def set_dnd_end(self, dnd_end: str) -> bool:
|
|
"""Set do not disturb function"""
|
|
time_pattern = re.compile("([0-1][0-9]|2[0-3]):[0-5][0-9]$")
|
|
if not re.match(time_pattern, dnd_end):
|
|
raise InvalidValueException(
|
|
"DND end time is not valid: (%s).", dnd_end)
|
|
return self.set_property(DreameVacuumProperty.DND_END, dnd_end)
|
|
|
|
def set_self_clean_area(self, self_clean_area: int) -> bool:
|
|
"""Set self clean area."""
|
|
if self.status.self_wash_base_available:
|
|
values = DreameVacuumDevice.split_group_value(self.get_property(DreameVacuumProperty.CLEANING_MODE), self.status.mop_pad_lifting_available)
|
|
if values and len(values) == 3:
|
|
values[1] = self_clean_area
|
|
return self.set_property(DreameVacuumProperty.CLEANING_MODE, DreameVacuumDevice.combine_group_value(values))
|
|
|
|
def locate(self) -> dict[str, Any] | None:
|
|
"""Locate the vacuum cleaner."""
|
|
return self.call_action(DreameVacuumAction.LOCATE)
|
|
|
|
def start(self) -> dict[str, Any] | None:
|
|
"""Start or resume the cleaning task."""
|
|
self.schedule_update(10)
|
|
|
|
if self.status.fast_mapping_paused:
|
|
return self.start_custom(DreameVacuumStatus.FAST_MAPPING.value)
|
|
|
|
if not self.status.started:
|
|
self._update_status(DreameVacuumTaskStatus.AUTO_CLEANING, DreameVacuumStatus.CLEANING)
|
|
|
|
if self._map_manager:
|
|
self._map_manager.editor.refresh_map()
|
|
return self.call_action(DreameVacuumAction.START)
|
|
|
|
def start_custom(self, status, parameters: dict[str, Any] = None) -> dict[str, Any] | None:
|
|
"""Start custom cleaning task."""
|
|
if status != DreameVacuumStatus.FAST_MAPPING.value and self.status.fast_mapping:
|
|
raise InvalidActionException(
|
|
"Cannot start cleaning while fast mapping")
|
|
|
|
payload = [{"piid": PIID(
|
|
DreameVacuumProperty.STATUS, self.property_mapping), "value": status}]
|
|
|
|
if parameters is not None:
|
|
payload.append(
|
|
{
|
|
"piid": PIID(DreameVacuumProperty.CLEANING_PROPERTIES, self.property_mapping),
|
|
"value": parameters,
|
|
}
|
|
)
|
|
|
|
return self.call_action(DreameVacuumAction.START_CUSTOM, payload)
|
|
|
|
def stop(self) -> dict[str, Any] | None:
|
|
"""Stop the vacuum cleaner."""
|
|
self.schedule_update(10)
|
|
|
|
if self.status.fast_mapping:
|
|
return self.return_to_base()
|
|
|
|
if self.status.started:
|
|
# Clear active segments on current map data
|
|
if self._map_manager:
|
|
self._map_manager.editor.set_active_segments([])
|
|
|
|
self._update_status(DreameVacuumTaskStatus.COMPLETED, DreameVacuumStatus.STANDBY)
|
|
return self.call_action(DreameVacuumAction.STOP)
|
|
|
|
def pause(self) -> dict[str, Any] | None:
|
|
"""Pause the cleaning task."""
|
|
if not self.status.paused and self.status.started:
|
|
self._update_property(
|
|
DreameVacuumProperty.STATE, DreameVacuumState.PAUSED.value
|
|
)
|
|
|
|
return self.call_action(DreameVacuumAction.PAUSE)
|
|
|
|
def return_to_base(self) -> dict[str, Any] | None:
|
|
"""Set the vacuum cleaner to return to the dock."""
|
|
if self.status.started:
|
|
self._update_property(
|
|
DreameVacuumProperty.STATE, DreameVacuumState.RETURNING.value
|
|
)
|
|
|
|
# Clear active segments on current map data
|
|
#if self._map_manager:
|
|
# self._map_manager.editor.set_active_segments([])
|
|
if self._map_manager:
|
|
self._map_manager.editor.refresh_map()
|
|
return self.call_action(DreameVacuumAction.CHARGE)
|
|
|
|
def start_pause(self) -> dict[str, Any] | None:
|
|
"""Start or resume the cleaning task."""
|
|
if (
|
|
not self.status.started
|
|
or self.status.state is DreameVacuumState.PAUSED
|
|
or self.status.status is DreameVacuumStatus.BACK_HOME
|
|
):
|
|
return self.start()
|
|
return self.pause()
|
|
|
|
def clean_zone(self, zones: list[int] | list[list[int]], cleaning_times: int) -> dict[str, Any] | None:
|
|
"""Clean selected area."""
|
|
self.schedule_update(10)
|
|
if zones and not isinstance(zones[0], list):
|
|
zones = [zones]
|
|
|
|
suction_level = self.status.suction_level.value
|
|
if self.status.self_wash_base_available:
|
|
water_volume = self.status.mop_pad_humidity.value
|
|
else:
|
|
water_volume = self.status.water_volume.value
|
|
|
|
cleanlist = []
|
|
for zone in zones:
|
|
cleanlist.append(
|
|
[
|
|
int(round(zone[0])),
|
|
int(round(zone[1])),
|
|
int(round(zone[2])),
|
|
int(round(zone[3])),
|
|
cleaning_times,
|
|
suction_level,
|
|
water_volume,
|
|
]
|
|
)
|
|
|
|
if not self.status.started or self.status.paused:
|
|
if self._map_manager:
|
|
# Set active areas on current map data is implemented on the app
|
|
self._map_manager.editor.set_active_areas(zones)
|
|
|
|
self._update_status(DreameVacuumTaskStatus.ZONE_CLEANING, DreameVacuumStatus.ZONE_CLEANING)
|
|
|
|
return self.start_custom(
|
|
DreameVacuumStatus.ZONE_CLEANING.value,
|
|
str(json.dumps({"areas": cleanlist}, separators=(",", ":"))).replace(
|
|
" ", ""
|
|
),
|
|
)
|
|
|
|
def clean_segment(self, selected_segments: int | list[int], cleaning_times: int | list[int], suction_level: int | list[int], water_volume: int | list[int]) -> dict[str, Any] | None:
|
|
"""Clean selected segment using id."""
|
|
self.schedule_update(10)
|
|
if not isinstance(selected_segments, list):
|
|
selected_segments = [selected_segments]
|
|
|
|
if not suction_level or suction_level == "":
|
|
suction_level = self.status.suction_level.value
|
|
|
|
if not water_volume or water_volume == "":
|
|
if self.status.self_wash_base_available:
|
|
water_volume = self.status.mop_pad_humidity.value
|
|
else:
|
|
water_volume = self.status.water_volume.value
|
|
|
|
cleanlist = []
|
|
index = 0
|
|
segments = self.status.segments
|
|
custom_order = self.get_property(DreameVacuumProperty.CLEANING_MODE) is not None and self.status.custom_order
|
|
|
|
for segment_id in selected_segments:
|
|
if not cleaning_times:
|
|
if (
|
|
segments and segment_id in segments and self.status.customized_cleaning
|
|
):
|
|
repeat = segments[segment_id].cleaning_times
|
|
else:
|
|
repeat = 1
|
|
elif isinstance(cleaning_times, list):
|
|
repeat = cleaning_times[index]
|
|
else:
|
|
repeat = cleaning_times
|
|
|
|
if not suction_level:
|
|
if (
|
|
segments and segment_id in segments and self.status.customized_cleaning
|
|
):
|
|
fan = segments[segment_id].suction_level
|
|
else:
|
|
fan = 1
|
|
elif isinstance(suction_level, list):
|
|
fan = suction_level[index]
|
|
else:
|
|
fan = suction_level
|
|
|
|
if not water_volume:
|
|
if (
|
|
segments and segment_id in segments and self.status.customized_cleaning
|
|
):
|
|
water = segments[segment_id].water_volume
|
|
else:
|
|
water = 1
|
|
elif isinstance(water_volume, list):
|
|
water = water_volume[index]
|
|
else:
|
|
water = water_volume
|
|
|
|
cleanlist.append(
|
|
[segment_id, repeat, fan, water,
|
|
1 if custom_order else (index + 1)]
|
|
)
|
|
index = index + 1
|
|
|
|
if not self.status.started or self.status.paused:
|
|
if self._map_manager:
|
|
# Set active segments on current map data is implemented on the app
|
|
self._map_manager.editor.set_active_segments(selected_segments)
|
|
|
|
self._update_status(DreameVacuumTaskStatus.SEGMENT_CLEANING, DreameVacuumStatus.SEGMENT_CLEANING)
|
|
|
|
return self.start_custom(
|
|
DreameVacuumStatus.SEGMENT_CLEANING.value,
|
|
str(json.dumps({"selects": cleanlist}, separators=(",", ":"))).replace(
|
|
" ", ""
|
|
),
|
|
)
|
|
|
|
def clean_spot(self, points: list[int] | list[list[int]], cleaning_times: int | list[int], suction_level: int | list[int], water_volume: int | list[int]) -> dict[str, Any] | None:
|
|
self.schedule_update(10)
|
|
if points and not isinstance(points[0], list):
|
|
points = [points]
|
|
|
|
suction_level = self.status.suction_level.value
|
|
if self.status.self_wash_base_available:
|
|
water_volume = self.status.mop_pad_humidity.value
|
|
else:
|
|
water_volume = self.status.water_volume.value
|
|
|
|
cleanlist = []
|
|
for point in points:
|
|
cleanlist.append(
|
|
[
|
|
int(round(point[0])),
|
|
int(round(point[1])),
|
|
cleaning_times,
|
|
suction_level,
|
|
water_volume,
|
|
]
|
|
)
|
|
|
|
if not self.status.started or self.status.paused:
|
|
if self._map_manager:
|
|
# Set active points on current map data is implemented on the app
|
|
self._map_manager.editor.set_active_points(points)
|
|
|
|
self._update_status(DreameVacuumTaskStatus.SPOT_CLEANING, DreameVacuumStatus.SPOT_CLEANING)
|
|
|
|
return self.start_custom(
|
|
DreameVacuumStatus.SPOT_CLEANING.value,
|
|
str(json.dumps({"points": cleanlist}, separators=(",", ":"))).replace(
|
|
" ", ""
|
|
),
|
|
)
|
|
|
|
def start_fast_mapping(self) -> dict[str, Any] | None:
|
|
"""Fast map."""
|
|
self.schedule_update(10)
|
|
if self.status.fast_mapping:
|
|
return
|
|
|
|
if self.status.battery_level < 15:
|
|
raise InvalidActionException(
|
|
"Low battery capacity. Please start the robot for working after it being fully charged."
|
|
)
|
|
|
|
if not self.status.self_wash_base_available and self.status.water_tank_or_mop_installed and not self.status.mop_pad_lifting_available:
|
|
raise InvalidActionException(
|
|
"Please make sure the mop pad is not installed before fast mapping."
|
|
)
|
|
|
|
self._update_status(DreameVacuumTaskStatus.FAST_MAPPING, DreameVacuumStatus.FAST_MAPPING)
|
|
|
|
if self._map_manager:
|
|
self._map_manager.editor.refresh_map()
|
|
|
|
return self.start_custom(DreameVacuumStatus.FAST_MAPPING.value)
|
|
|
|
def start_mapping(self) -> dict[str, Any] | None:
|
|
"""Create a new map by cleaning whole floor."""
|
|
self.schedule_update(10)
|
|
if self._map_manager:
|
|
self._map_manager.editor.reset_map()
|
|
self._update_status(DreameVacuumTaskStatus.AUTO_CLEANING, DreameVacuumStatus.CLEANING)
|
|
|
|
return self.start_custom(DreameVacuumStatus.CLEANING.value, "3")
|
|
|
|
def start_self_wash_base(self, parameters: dict[str, Any] = None) -> dict[str, Any] | None:
|
|
"""Start self-wash base for cleaning or drying the mop."""
|
|
if not self.status.self_wash_base_available:
|
|
return
|
|
|
|
if self.info and self.info.version <= 1037:
|
|
parameters = None
|
|
|
|
payload = None
|
|
if parameters is not None:
|
|
payload = [
|
|
{
|
|
"piid": PIID(DreameVacuumProperty.CLEANING_PROPERTIES, self.property_mapping),
|
|
"value": parameters,
|
|
}
|
|
]
|
|
return self.call_action(DreameVacuumAction.START_WASHING, payload)
|
|
|
|
def start_washing(self) -> dict[str, Any] | None:
|
|
"""Start washing the mop if self-wash base is present."""
|
|
if self.status.washing_paused:
|
|
if self.info and self.info.version <= 1037:
|
|
return self.start()
|
|
return self.start_self_wash_base("1,1")
|
|
if self.status.washing_available or self.status.returning_to_wash_paused:
|
|
return self.start_self_wash_base("2,1")
|
|
|
|
def pause_washing(self) -> dict[str, Any] | None:
|
|
"""Pause washing the mop if self-wash base is present."""
|
|
if self.status.washing:
|
|
if self.info and self.info.version <= 1037:
|
|
return self.pause()
|
|
return self.start_self_wash_base("1,0")
|
|
|
|
def start_drying(self) -> dict[str, Any] | None:
|
|
"""Start drying the mop if self-wash base is present."""
|
|
if self.status.drying_available and not self.status.drying:
|
|
return self.start_self_wash_base("3,1")
|
|
|
|
def stop_drying(self) -> dict[str, Any] | None:
|
|
"""Stop drying the mop if self-wash base is present."""
|
|
if self.status.drying_available and self.status.drying:
|
|
return self.start_self_wash_base("3,0")
|
|
|
|
def clear_warning(self) -> dict[str, Any] | None:
|
|
"""Clear warning error code from the vacuum cleaner."""
|
|
if self.status.has_warning:
|
|
return self.call_action(
|
|
DreameVacuumAction.CLEAR_WARNING,
|
|
[{"piid": PIID(DreameVacuumProperty.CLEANING_PROPERTIES,
|
|
self.property_mapping), "value": f"[{self.status.error.value}]"}],
|
|
)
|
|
|
|
def remote_control_move_step(
|
|
self, rotation: int = 0, velocity: int = 0
|
|
) -> dict[str, Any] | None:
|
|
"""Send remote control command to device."""
|
|
if self.status.fast_mapping:
|
|
raise InvalidActionException(
|
|
"Cannot remote control vacuum while fast mapping"
|
|
)
|
|
|
|
if self.status.washing:
|
|
raise InvalidActionException(
|
|
"Cannot remote control vacuum while self-wash base is running"
|
|
)
|
|
|
|
payload = (
|
|
'{"spdv":%(velocity)d,"spdw":%(rotation)d,"audio":"%(audio)s","random":%(random)d}'
|
|
% {
|
|
"velocity": velocity,
|
|
"rotation": rotation,
|
|
"audio": "false" if self._remote_control or self.status.status is DreameVacuumStatus.SLEEPING else "true",
|
|
"random": randrange(65535),
|
|
}
|
|
)
|
|
self._remote_control = True
|
|
mapping = self.property_mapping[DreameVacuumProperty.REMOTE_CONTROL]
|
|
return self._protocol.set_property(mapping["siid"], mapping["piid"], payload, 0)
|
|
|
|
def install_voice_pack(self, lang_id: int, url: str, md5: str, size: int) -> dict[str, Any] | None:
|
|
"""install a custom language pack"""
|
|
payload = (
|
|
'{"id":"%(lang_id)s","url":"%(url)s","md5":"%(md5)s","size":%(size)d}'
|
|
% {"lang_id": lang_id, "url": url, "md5": md5, "size": size}
|
|
)
|
|
mapping = self.property_mapping[DreameVacuumProperty.VOICE_CHANGE]
|
|
return self._protocol.set_property(mapping["siid"], mapping["piid"], payload, 1)
|
|
|
|
def set_ai_detection(self, settings: dict[str, bool] | int) -> dict[str, Any] | None:
|
|
"""Send ai detection parameters to the device."""
|
|
if self.status.ai_detection_available:
|
|
self._property_changed()
|
|
|
|
if (
|
|
(self.status.ai_obstacle_detection or self.status.obstacle_image_upload)
|
|
and (self._protocol.cloud and not self.status.ai_policy_accepted)
|
|
):
|
|
prop = "prop.s_ai_config"
|
|
response = self._protocol.cloud.get_batch_device_datas([prop])
|
|
if response and prop in response and response[prop]:
|
|
try:
|
|
self.status.ai_policy_accepted = json.loads(
|
|
response[prop]).get("privacyAuthed")
|
|
except:
|
|
pass
|
|
|
|
if not self.status.ai_policy_accepted:
|
|
if self.status.ai_obstacle_detection:
|
|
self.status.ai_obstacle_detection = False
|
|
|
|
if self.status.obstacle_image_upload:
|
|
self.status.obstacle_image_upload = False
|
|
|
|
self._property_changed()
|
|
|
|
raise InvalidActionException(
|
|
"You need to accept privacy policy from the App before enabling AI obstacle detection feature"
|
|
)
|
|
|
|
mapping = self.property_mapping[DreameVacuumProperty.AI_DETECTION]
|
|
if isinstance(settings, int):
|
|
return self._protocol.set_property(mapping["siid"], mapping["piid"], settings, 1)
|
|
return self._protocol.set_property(mapping["siid"], mapping["piid"], str(json.dumps(settings, separators=(",", ":"))).replace(" ", ""), 1)
|
|
|
|
def set_ai_obstacle_detection(self, enabled: bool) -> dict[str, Any] | None:
|
|
"""Enable or disable AI detection feature."""
|
|
if self.status.ai_detection_available:
|
|
current_value = self.status.ai_obstacle_detection
|
|
self.status.ai_obstacle_detection = bool(enabled)
|
|
value = self.get_property(DreameVacuumProperty.AI_DETECTION)
|
|
if isinstance(value, int):
|
|
value = (value | 2) if self.status.ai_obstacle_detection else (value & -3)
|
|
result = self.set_ai_detection(value)
|
|
else:
|
|
result = self.set_ai_detection(
|
|
{AI_SETTING_SWITCH: self.status.ai_obstacle_detection})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.ai_obstacle_detection = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_obstacle_image_upload(self, enabled: bool) -> dict[str, Any] | None:
|
|
"""Enable or disable obstacle picture uploading to the cloud."""
|
|
if self.status.ai_detection_available:
|
|
current_value = self.status.obstacle_image_upload
|
|
self.status.obstacle_image_upload = bool(enabled)
|
|
value = self.get_property(DreameVacuumProperty.AI_DETECTION)
|
|
if isinstance(value, int):
|
|
value = (value | 32) if self.status.obstacle_image_upload else (value & -33)
|
|
result = self.set_ai_detection(value)
|
|
else:
|
|
result = self.set_ai_detection(
|
|
{AI_SETTING_UPLOAD: self.status.obstacle_image_upload})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.obstacle_image_upload = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_pet_detection(self, enabled: bool) -> dict[str, Any] | None:
|
|
"""Enable or disable AI pet detection feature."""
|
|
if self.status.ai_detection_available:
|
|
current_value = self.status.pet_detection
|
|
self.status.pet_detection = bool(enabled)
|
|
value = self.get_property(DreameVacuumProperty.AI_DETECTION)
|
|
if isinstance(value, int):
|
|
value = (value | 16) if self.status.pet_detection else (value & -17)
|
|
result = self.set_ai_detection(value)
|
|
else:
|
|
result = self.set_ai_detection(
|
|
{AI_SETTING_PET: self.status.pet_detection})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.pet_detection = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_human_detection(self, enabled: bool) -> dict[str, Any] | None:
|
|
"""Enable or disable AI human detection feature."""
|
|
if self.status.ai_detection_available:
|
|
current_value = self.status.human_detection
|
|
self.status.human_detection = bool(enabled)
|
|
value = self.get_property(DreameVacuumProperty.AI_DETECTION)
|
|
if isinstance(value, int):
|
|
return None
|
|
else:
|
|
result = self.set_ai_detection(
|
|
{AI_SETTING_HUMAN: self.status.human_detection})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.human_detection = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_furniture_detection(self, enabled: bool) -> dict[str, Any] | None:
|
|
"""Enable or disable AI furnitue detection feature."""
|
|
if self.status.ai_detection_available:
|
|
current_value = self.status.furniture_detection
|
|
self.status.furniture_detection = bool(enabled)
|
|
value = self.get_property(DreameVacuumProperty.AI_DETECTION)
|
|
if isinstance(value, int):
|
|
value = (value | 1) if self.status.furniture_detection else (value & -2)
|
|
result = self.set_ai_detection(value)
|
|
else:
|
|
result = self.set_ai_detection(
|
|
{AI_SETTING_FURNITURE: self.status.furniture_detection})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.furniture_detection = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_fluid_detection(self, enabled: bool) -> dict[str, Any] | None:
|
|
"""Enable or disable AI fluid detection feature."""
|
|
if self.status.ai_detection_available:
|
|
current_value = self.status.fluid_detection
|
|
self.status.fluid_detection = bool(enabled)
|
|
value = self.get_property(DreameVacuumProperty.AI_DETECTION)
|
|
if isinstance(value, int):
|
|
value = (value | 8) if self.status.fluid_detection else (value & -9)
|
|
result = self.set_ai_detection(value)
|
|
else:
|
|
result = self.set_ai_detection(
|
|
{AI_SETTING_FLUID: self.status.fluid_detection})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.fluid_detection = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_obstacle_picture(self, enabled: bool) -> dict[str, Any] | None:
|
|
"""Enable or disable AI obstacle picture display feature."""
|
|
if self.status.ai_detection_available:
|
|
current_value = self.status.obstacle_picture
|
|
self.status.obstacle_picture = bool(enabled)
|
|
value = self.get_property(DreameVacuumProperty.AI_DETECTION)
|
|
if isinstance(value, int):
|
|
value = (value | 4) if self.status.obstacle_picture else (value & -5)
|
|
result = self.set_ai_detection(value)
|
|
else:
|
|
result = self.set_ai_detection(
|
|
{AI_SETTING_FLUID: self.status.obstacle_picture})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.obstacle_picture = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_auto_switch_settings(self, settings) -> dict[str, Any] | None:
|
|
if self.status.ai_detection_available:
|
|
self._property_changed()
|
|
|
|
mapping = self.property_mapping[DreameVacuumProperty.AUTO_SWITCH_SETTINGS]
|
|
return self._protocol.set_property(mapping["siid"], mapping["piid"], str(json.dumps(settings, separators=(",", ":"))).replace(" ", ""), 1)
|
|
|
|
def set_fill_light(self, enabled: bool) -> dict[str, Any] | None:
|
|
if self.status.auto_switch_settings_available:
|
|
current_value = self.status.fill_light
|
|
self.status.fill_light = 1 if enabled else 0
|
|
result = self.set_auto_switch_settings({"k": "FillinLight", "v": self.status.fill_light})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.fill_light = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_collision_avoidance(self, enabled: bool) -> dict[str, Any] | None:
|
|
if self.status.auto_switch_settings_available:
|
|
current_value = self.status.collision_avoidance
|
|
self.status.collision_avoidance = 1 if enabled else 0
|
|
result = self.set_auto_switch_settings({"k": "LessColl", "v": self.status.collision_avoidance})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.collision_avoidance = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_auto_drying(self, enabled: bool) -> dict[str, Any] | None:
|
|
if self.status.auto_switch_settings_available:
|
|
current_value = self.status.auto_drying
|
|
self.status.auto_drying = 1 if enabled else 0
|
|
result = self.set_auto_switch_settings({"k": "AutoDry", "v": self.status.auto_drying})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.auto_drying = current_value
|
|
self._property_changed()
|
|
return result
|
|
else:
|
|
current_value = self.status.auto_drying
|
|
self.status.auto_drying = 1 if enabled else 0
|
|
if not self.set_property(DreameVacuumProperty.INTELLIGENT_RECOGNITION, self.status.auto_drying):
|
|
self.status.auto_drying = current_value
|
|
self._property_changed()
|
|
return False
|
|
return True
|
|
|
|
def set_stain_avoidance(self, stain_avoidance: int) -> dict[str, Any] | None:
|
|
if self.status.auto_switch_settings_available:
|
|
current_value = self.status.stain_avoidance
|
|
self.status.stain_avoidance = stain_avoidance
|
|
result = self.set_auto_switch_settings({"k": "StainIdentify", "v": self.status.stain_avoidance})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.stain_avoidance = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_mopping_type(self, mopping_type: int) -> dict[str, Any] | None:
|
|
if self.status.auto_switch_settings_available:
|
|
current_value = self.status.mopping_type
|
|
self.status.mopping_type = mopping_type
|
|
result = self.set_auto_switch_settings({"k": "CleanType", "v": self.status.mopping_type})
|
|
if result and result[0]["code"] != 0:
|
|
self.status.mopping_type = current_value
|
|
self._property_changed()
|
|
return result
|
|
|
|
def set_multi_map(self, enabled: bool) -> bool:
|
|
if self.set_property(DreameVacuumProperty.MULTI_FLOOR_MAP, int(enabled)):
|
|
if self.status.auto_switch_settings_available and not enabled and self.get_property(DreameVacuumProperty.INTELLIGENT_RECOGNITION) == 1:
|
|
self.set_property(DreameVacuumProperty.INTELLIGENT_RECOGNITION, 0)
|
|
return True
|
|
return False
|
|
|
|
def request_map(self) -> dict[str, Any] | None:
|
|
"""Send map request action to the device.
|
|
Device will upload a new map on cloud after this command if it has a saved map on memory.
|
|
Otherwise this action will timeout when device is spot cleaning or a restored map exists on memory."""
|
|
|
|
if self._map_manager:
|
|
return self._map_manager.request_new_map()
|
|
return self.call_action(
|
|
DreameVacuumAction.REQUEST_MAP, [{"piid": PIID(
|
|
DreameVacuumProperty.FRAME_INFO, self.property_mapping), "value": '{"frame_type":"I"}'}]
|
|
)
|
|
|
|
def update_map_data(self, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Send update map action to the device."""
|
|
if self._map_manager:
|
|
self._map_manager.schedule_update(10)
|
|
self._property_changed()
|
|
self._last_map_request = time.time()
|
|
|
|
response = self.call_action(
|
|
DreameVacuumAction.UPDATE_MAP_DATA,
|
|
[
|
|
{
|
|
"piid": PIID(DreameVacuumProperty.MAP_EXTEND_DATA, self.property_mapping),
|
|
"value": str(json.dumps(parameters, separators=(",", ":"))).replace(
|
|
" ", ""
|
|
),
|
|
}
|
|
],
|
|
)
|
|
|
|
self.schedule_update(5)
|
|
|
|
if self._map_manager:
|
|
self._map_manager.request_next_map()
|
|
self._last_map_list_request = 0
|
|
|
|
return response
|
|
|
|
def rename_map(self, map_id: int, map_name: str = "") -> dict[str, Any] | None:
|
|
"""Set custom name for a map"""
|
|
if self.status.has_temporary_map:
|
|
raise InvalidActionException(
|
|
"Cannot rename a map when temporary map is present"
|
|
)
|
|
|
|
if map_name != "":
|
|
map_name = map_name.replace(" ", "-")
|
|
if self._map_manager:
|
|
self._map_manager.editor.set_map_name(map_id, map_name)
|
|
return self.update_map_data({"nrism": {map_id: {"name": map_name}}})
|
|
|
|
def set_map_rotation(self, map_id: int, rotation: int) -> dict[str, Any] | None:
|
|
"""Set rotation of a map"""
|
|
if self.status.has_temporary_map:
|
|
raise InvalidActionException(
|
|
"Cannot rotate a map when temporary map is present"
|
|
)
|
|
|
|
if rotation is not None:
|
|
rotation = int(rotation)
|
|
if rotation > 270 or rotation < 0:
|
|
rotation = 0
|
|
|
|
if self._map_manager:
|
|
self._map_manager.editor.set_rotation(map_id, rotation)
|
|
return self.update_map_data({"smra": {map_id: {"ra": rotation}}})
|
|
|
|
def set_restricted_zone(self, walls=[], zones=[], no_mops=[]) -> dict[str, Any] | None:
|
|
"""Set restricted zones on current map."""
|
|
if self._map_manager:
|
|
self._map_manager.editor.set_zones(walls, zones, no_mops)
|
|
return self.update_map_data({"vw": {"line": walls, "rect": zones, "mop": no_mops}})
|
|
|
|
def select_map(self, map_id: int) -> dict[str, Any] | None:
|
|
"""Change currently selected map when multi floor map is enabled."""
|
|
if self.status.multi_map:
|
|
if self._map_manager:
|
|
self._map_manager.editor.select_map(map_id)
|
|
return self.update_map_data({"sm": {}, "mapid": map_id})
|
|
|
|
def delete_map(self, map_id: int = None) -> dict[str, Any] | None:
|
|
"""Delete a map."""
|
|
if self.status.has_temporary_map:
|
|
raise InvalidActionException(
|
|
"Cannot delete a map when temporary map is present"
|
|
)
|
|
|
|
if self.status.started:
|
|
raise InvalidActionException(
|
|
"Cannot delete a map while vacuum is running")
|
|
|
|
if self._map_manager:
|
|
if map_id == 0:
|
|
map_id = None
|
|
|
|
# Device do not deletes saved maps when you disable multi floor map feature
|
|
# but it deletes all maps if you delete any map when multi floor map is disabled.
|
|
if self.status.multi_map:
|
|
if not map_id and self._map_manager.selected_map:
|
|
map_id = self._map_manager.selected_map.map_id
|
|
else:
|
|
if (
|
|
(self._map_manager.selected_map and map_id == self._map_manager.selected_map.map_id)
|
|
):
|
|
self._map_manager.editor.delete_map()
|
|
else:
|
|
self._map_manager.editor.delete_map(map_id)
|
|
parameters = {"cm": {}}
|
|
if map_id:
|
|
parameters["mapid"] = map_id
|
|
return self.update_map_data(parameters)
|
|
|
|
def save_temporary_map(self) -> dict[str, Any] | None:
|
|
"""Replace new map with an old one when multi floor map is disabled."""
|
|
if self.status.has_temporary_map:
|
|
if self._map_manager:
|
|
self._map_manager.editor.save_temporary_map()
|
|
return self.update_map_data({"cw": 5})
|
|
|
|
def discard_temporary_map(self) -> dict[str, Any] | None:
|
|
"""Discard new map when device have reached maximum number of maps it can store."""
|
|
if self.status.has_temporary_map:
|
|
if self._map_manager:
|
|
self._map_manager.editor.discard_temporary_map()
|
|
return self.update_map_data({"cw": 0})
|
|
|
|
def replace_temporary_map(self, map_id: int = None) -> dict[str, Any] | None:
|
|
"""Replace new map with an old one when device have reached maximum number of maps it can store."""
|
|
if self.status.has_temporary_map:
|
|
if self.status.multi_map:
|
|
raise InvalidActionException(
|
|
"Cannot replace a map when multi floor map is disabled"
|
|
)
|
|
|
|
if self._map_manager:
|
|
self._map_manager.editor.replace_temporary_map(map_id)
|
|
parameters = {"cw": 1}
|
|
if map_id:
|
|
parameters["mapid"] = map_id
|
|
return self.update_map_data(parameters)
|
|
|
|
def restore_map(self, map_id: int, map_url: str) -> dict[str, Any] | None:
|
|
"""Replace a map with previously saved version by device."""
|
|
|
|
if not self.status.has_temporary_map:
|
|
if self._map_manager:
|
|
self._map_manager.editor.restore_map(map_id, map_url)
|
|
self._last_map_request = time.time()
|
|
self._map_manager.schedule_update(10)
|
|
|
|
mapping = self.property_mapping[DreameVacuumProperty.MAP_RECOVERY]
|
|
response = self._protocol.set_property(mapping["siid"], mapping["piid"], str(
|
|
json.dumps({"map_id": map_id, "map_url": map_url}, separators=(",", ":"))).replace(" ", ""), 1)
|
|
|
|
if self._map_manager:
|
|
self._map_manager.request_next_map()
|
|
return response
|
|
|
|
def merge_segments(self, map_id: int, segments: list[int]) -> dict[str, Any] | None:
|
|
"""Merge segments on a map"""
|
|
if self.status.has_temporary_map:
|
|
raise InvalidActionException(
|
|
"Cannot edit segments when temporary map is present"
|
|
)
|
|
|
|
if segments:
|
|
if map_id == "":
|
|
map_id = None
|
|
|
|
if self._map_manager:
|
|
if not map_id:
|
|
if self.status.lidar_navigation and self._map_manager.selected_map:
|
|
map_id = self._map_manager.selected_map.map_id
|
|
else:
|
|
map_id = 0
|
|
self._map_manager.editor.merge_segments(map_id, segments)
|
|
|
|
if not map_id and self.status.lidar_navigation:
|
|
raise InvalidActionException(
|
|
"Map ID is required"
|
|
)
|
|
|
|
data = { "msr": [segments[0], segments[1]] }
|
|
if map_id:
|
|
data["mapid"] = map_id
|
|
return self.update_map_data(data)
|
|
|
|
def split_segments(self, map_id: int, segment: int, line: list[int]) -> dict[str, Any] | None:
|
|
"""Split segments on a map"""
|
|
if self.status.has_temporary_map:
|
|
raise InvalidActionException(
|
|
"Cannot edit segments when temporary map is present"
|
|
)
|
|
|
|
if segment and line is not None:
|
|
if map_id == "":
|
|
map_id = None
|
|
|
|
if self._map_manager:
|
|
if not map_id:
|
|
if self.status.lidar_navigation and self._map_manager.selected_map:
|
|
map_id = self._map_manager.selected_map.map_id
|
|
else:
|
|
map_id = 0
|
|
self._map_manager.editor.split_segments(map_id, segment, line)
|
|
|
|
if not map_id and self.status.lidar_navigation:
|
|
raise InvalidActionException(
|
|
"Map ID is required"
|
|
)
|
|
|
|
line.append(segment)
|
|
data = { "dsrid": line }
|
|
if map_id:
|
|
data["mapid"] = map_id
|
|
return self.update_map_data(data)
|
|
|
|
def set_cleaning_sequence(self, cleaning_sequence: list[int]) -> dict[str, Any] | None:
|
|
"""Set cleaning sequence on current map.
|
|
Device will use this order even you specify order in segment cleaning."""
|
|
|
|
if self.status.has_temporary_map:
|
|
raise InvalidActionException(
|
|
"Cannot edit segments when temporary map is present"
|
|
)
|
|
|
|
if self.status.started:
|
|
raise InvalidActionException(
|
|
"Cannot set cleaning sequence while vacuum is running"
|
|
)
|
|
|
|
if cleaning_sequence == "" or not cleaning_sequence:
|
|
cleaning_sequence = []
|
|
|
|
if self._map_manager:
|
|
if cleaning_sequence and self.status.segments and len(cleaning_sequence) != len(self.status.segments.items()):
|
|
raise InvalidValueException("Invalid size for cleaning sequence")
|
|
|
|
cleaning_sequence = self._map_manager.editor.set_cleaning_sequence(cleaning_sequence)
|
|
|
|
return self.update_map_data({"cleanOrder": cleaning_sequence})
|
|
|
|
def set_cleanset(self, cleanset: dict[str, list[int]]) -> dict[str, Any] | None:
|
|
"""Set customized cleaning settings on current map.
|
|
Device will use these settings even you pass another setting for custom segment cleaning."""
|
|
|
|
if self.status.has_temporary_map:
|
|
raise InvalidActionException(
|
|
"Cannot edit segments when temporary map is present"
|
|
)
|
|
|
|
if cleanset is not None:
|
|
return self.update_map_data({"customeClean": cleanset})
|
|
|
|
def set_custom_cleaning(self, segment_id: list[int], suction_level: list[int], water_volume: list[int], cleaning_times: list[int]) -> dict[str, Any] | None:
|
|
"""Set customized cleaning settings on current map.
|
|
Device will use these settings even you pass another setting for custom segment cleaning."""
|
|
|
|
if (
|
|
segment_id != ""
|
|
and segment_id
|
|
and suction_level != ""
|
|
and suction_level
|
|
and water_volume != ""
|
|
and water_volume
|
|
and cleaning_times != ""
|
|
and cleaning_times is not None
|
|
):
|
|
segments = self.status.segments
|
|
if segments:
|
|
count = len(segments.items())
|
|
if (
|
|
len(segment_id) != count
|
|
or len(suction_level) != count
|
|
or len(water_volume) != count
|
|
or len(cleaning_times) != cleaning_times
|
|
):
|
|
return
|
|
|
|
custom_cleaning = []
|
|
index = 0
|
|
for segment in segment_id:
|
|
custom_cleaning.append(
|
|
# for some reason cleanset uses different int values for water volume
|
|
[segment, suction_level[index], water_volume[index] + 1, cleaning_times[index]]
|
|
)
|
|
index = index + 1
|
|
|
|
return self.set_cleanset(custom_cleaning)
|
|
|
|
def set_segment_name(self, segment_id: int, segment_type: int, custom_name: str = None) -> dict[str, Any] | None:
|
|
"""Update name of a segment on current map"""
|
|
if self.status.has_temporary_map:
|
|
raise InvalidActionException(
|
|
"Cannot edit segment when temporary map is present"
|
|
)
|
|
|
|
if self._map_manager:
|
|
segment_info = self._map_manager.editor.set_segment_name(
|
|
segment_id, segment_type, custom_name
|
|
)
|
|
if segment_info:
|
|
return self.update_map_data({"nsr": segment_info})
|
|
|
|
def set_segment_order(self, segment_id: int, order: int) -> dict[str, Any] | None:
|
|
"""Update cleaning order of a segment on current map"""
|
|
if self._map_manager and not self.status.has_temporary_map:
|
|
return self.update_map_data({"cleanOrder": self._map_manager.editor.set_segment_order(segment_id, order)})
|
|
|
|
def set_segment_suction_level(self, segment_id: int, suction_level: int) -> dict[str, Any] | None:
|
|
"""Update suction level of a segment on current map"""
|
|
if self._map_manager and not self.status.has_temporary_map:
|
|
return self.set_cleanset(
|
|
self._map_manager.editor.set_segment_suction_level(
|
|
segment_id, suction_level)
|
|
)
|
|
|
|
def set_segment_water_volume(self, segment_id: int, water_volume: int) -> dict[str, Any] | None:
|
|
"""Update water volume of a segment on current map"""
|
|
if not self.status.self_wash_base_available and self._map_manager and not self.status.has_temporary_map:
|
|
return self.set_cleanset(
|
|
self._map_manager.editor.set_segment_water_volume(
|
|
segment_id, water_volume)
|
|
)
|
|
|
|
def set_segment_mop_pad_humidity(self, segment_id: int, mop_pad_humidity: int) -> dict[str, Any] | None:
|
|
"""Update mop pad humidity of a segment on current map"""
|
|
if self.status.self_wash_base_available and self._map_manager and not self.status.has_temporary_map:
|
|
return self.set_cleanset(
|
|
self._map_manager.editor.set_segment_water_volume(
|
|
segment_id, mop_pad_humidity)
|
|
)
|
|
|
|
def set_segment_cleaning_times(self, segment_id: int, cleaning_times: int) -> dict[str, Any] | None:
|
|
"""Update cleaning times of a segment on current map."""
|
|
if self.status.started:
|
|
raise InvalidActionException(
|
|
"Cannot set room cleaning times while vacuum is running"
|
|
)
|
|
|
|
if self._map_manager and not self.status.has_temporary_map:
|
|
return self.set_cleanset(
|
|
self._map_manager.editor.set_segment_cleaning_times(
|
|
segment_id, cleaning_times)
|
|
)
|
|
|
|
@property
|
|
def _update_interval(self) -> float:
|
|
"""Dynamic update interval of the device for the timer."""
|
|
now = time.time()
|
|
if self._last_update_failed:
|
|
return 5 if now - self._last_update_failed <= 60 else 10 if now - self._last_update_failed <= 300 else 30
|
|
if not - self._last_change <= 60:
|
|
return 3 if self.status.active else 5
|
|
if self.status.active or self.status.started:
|
|
return 3 if self.status.running else 5
|
|
if self._map_manager:
|
|
return min(self._map_update_interval, 10)
|
|
return 10
|
|
|
|
@property
|
|
def _map_update_interval(self) -> float:
|
|
"""Dynamic map update interval for the map manager."""
|
|
if self._map_manager:
|
|
now = time.time()
|
|
if now - self._last_map_request <= 120 or now - self._last_change <= 60:
|
|
return 2.5 if self.status.active or self.status.started else 5
|
|
return 3 if self.status.running else 10 if self.status.active else 30
|
|
return -1
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the name of the device."""
|
|
return self._name
|
|
|
|
@property
|
|
def device_connected(self) -> bool:
|
|
"""Return connection status of the device."""
|
|
return self._protocol.connected
|
|
|
|
@property
|
|
def cloud_connected(self) -> bool:
|
|
"""Return connection status of the device."""
|
|
return (
|
|
self._protocol.cloud
|
|
and self._protocol.cloud.logged_in
|
|
and self._protocol.cloud.connected
|
|
)
|
|
|
|
|
|
class DreameVacuumDeviceStatus:
|
|
"""Helper class for device status and int enum type properties.
|
|
This class is used for determining various states of the device by its properties.
|
|
Determined states are used by multiple validation and rendering condition checks.
|
|
Almost of the rules are extracted from mobile app that has a similar class with same purpose."""
|
|
|
|
_cleaning_history = None
|
|
_last_cleaning_time = None
|
|
|
|
suction_level_list = {v: k for k, v in SUCTION_LEVEL_CODE_TO_NAME.items()}
|
|
water_volume_list = {v: k for k, v in WATER_VOLUME_CODE_TO_NAME.items()}
|
|
mop_pad_humidity_list = {v: k for k, v in MOP_PAD_HUMIDITY_CODE_TO_NAME.items()}
|
|
cleaning_mode_list = {v: k for k, v in CLEANING_MODE_CODE_TO_NAME.items()}
|
|
carpet_sensitivity_list = {v: k for k, v in CARPET_SENSITIVITY_CODE_TO_NAME.items()}
|
|
self_clean_area_list = {v: k for k, v in SELF_AREA_CLEAN_TO_NAME.items()}
|
|
mop_wash_level_list = {v: k for k, v in MOP_WASH_LEVEL_TO_NAME.items()}
|
|
mopping_type_list = {v: k for k, v in MOPPING_TYPE_TO_NAME.items()}
|
|
|
|
ai_policy_accepted = False
|
|
ai_obstacle_detection = None
|
|
obstacle_image_upload = None
|
|
pet_detection = None
|
|
human_detection = None
|
|
furniture_detection = None
|
|
fluid_detection = None
|
|
obstacle_picture = None
|
|
ai_picture = None
|
|
collision_avoidance = None
|
|
fill_light = None
|
|
auto_drying = None
|
|
stain_avoidance = None
|
|
mopping_type = None
|
|
|
|
lidar_navigation = None
|
|
ai_detection_available = None
|
|
self_wash_base_available = None
|
|
auto_empty_base_available = None
|
|
mop_pad_lifting_available = None
|
|
customized_cleaning_available = None
|
|
auto_switch_settings_available = None
|
|
robot_shape = None
|
|
|
|
def __init__(self, device):
|
|
self._device = device
|
|
|
|
def update_static_properties(self):
|
|
self.lidar_navigation = bool(self._get_property(DreameVacuumProperty.MAP_SAVING) is None)
|
|
self.ai_detection_available = bool(self._get_property(DreameVacuumProperty.AI_DETECTION) is not None)
|
|
self.self_wash_base_available = bool(self._get_property(DreameVacuumProperty.SELF_WASH_BASE_STATUS) is not None)
|
|
self.auto_empty_base_available = bool(self._get_property(DreameVacuumProperty.DUST_COLLECTION) is not None)
|
|
self.customized_cleaning_available = bool(self._get_property(DreameVacuumProperty.CUSTOMIZED_CLEANING) is not None)
|
|
self.auto_switch_settings_available = bool(self._get_property(DreameVacuumProperty.AUTO_SWITCH_SETTINGS) is not None)
|
|
self.mop_pad_lifting_available = bool((self.self_wash_base_available and self.auto_empty_base_available) or (self._device.info and "r2216" in self._device.info.model))
|
|
self.robot_shape = 2 if self.self_wash_base_available and not self.mop_pad_lifting_available else 0 if self.lidar_navigation else 1
|
|
|
|
def _get_property(self, prop: DreameVacuumProperty) -> Any:
|
|
"""Helper function for accessing a property from device"""
|
|
return self._device.get_property(prop)
|
|
|
|
@property
|
|
def _map_manager(self) -> DreameMapVacuumMapManager | None:
|
|
"""Helper property for accessing map manager from device"""
|
|
return self._device._map_manager
|
|
|
|
@property
|
|
def _device_connected(self) -> bool:
|
|
"""Helper property for accessing device connection status"""
|
|
return self._device.device_connected
|
|
|
|
@property
|
|
def battery_level(self) -> int:
|
|
"""Return battery level of the device."""
|
|
return self._get_property(DreameVacuumProperty.BATTERY_LEVEL)
|
|
|
|
@property
|
|
def suction_level(self) -> DreameVacuumSuctionLevel:
|
|
"""Return suction level of the device."""
|
|
value = self._get_property(DreameVacuumProperty.SUCTION_LEVEL)
|
|
if value is not None and value in DreameVacuumSuctionLevel._value2member_map_:
|
|
return DreameVacuumSuctionLevel(value)
|
|
_LOGGER.debug("SUCTION_LEVEL not supported: %s", value)
|
|
return DreameVacuumSuctionLevel.UNKNOWN
|
|
|
|
@property
|
|
def suction_level_name(self) -> str:
|
|
"""Return suction level as string for translation."""
|
|
return SUCTION_LEVEL_CODE_TO_NAME.get(self.suction_level, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def water_volume(self) -> DreameVacuumWaterVolume:
|
|
"""Return water volume of the device."""
|
|
value = self._get_property(DreameVacuumProperty.WATER_VOLUME)
|
|
if value is not None and value in DreameVacuumWaterVolume._value2member_map_:
|
|
return DreameVacuumWaterVolume(value)
|
|
_LOGGER.debug("WATER_VOLUME not supported: %s", value)
|
|
return DreameVacuumWaterVolume.UNKNOWN
|
|
|
|
@property
|
|
def water_volume_name(self) -> str:
|
|
"""Return water volume as string for translation."""
|
|
return WATER_VOLUME_CODE_TO_NAME.get(self.water_volume, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def mop_pad_humidity(self) -> DreameVacuumMopPadHumidity:
|
|
"""Return mop pad humidity of the device."""
|
|
if self.self_wash_base_available:
|
|
values = DreameVacuumDevice.split_group_value(self._get_property(DreameVacuumProperty.CLEANING_MODE))
|
|
if values and len(values) == 3:
|
|
value = values[2]
|
|
if value is not None and value in DreameVacuumMopPadHumidity._value2member_map_:
|
|
return DreameVacuumMopPadHumidity(value)
|
|
_LOGGER.debug("MOP_PAD_HUMIDITY not supported: %s", value)
|
|
return DreameVacuumMopPadHumidity.UNKNOWN
|
|
|
|
@property
|
|
def mop_pad_humidity_name(self) -> str:
|
|
"""Return mop pad humidity as string for translation."""
|
|
return MOP_PAD_HUMIDITY_CODE_TO_NAME.get(self.mop_pad_humidity, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def cleaning_mode(self) -> DreameVacuumCleaningMode:
|
|
"""Return cleaning mode of the device."""
|
|
value = self._get_property(DreameVacuumProperty.CLEANING_MODE)
|
|
if self.self_wash_base_available:
|
|
values = DreameVacuumDevice.split_group_value(value, self.mop_pad_lifting_available)
|
|
if values and len(values) == 3:
|
|
if not self.mop_pad_lifting_available:
|
|
if not self.water_tank_or_mop_installed:
|
|
return DreameVacuumCleaningMode.SWEEPING
|
|
if values[0] == 1:
|
|
return DreameVacuumCleaningMode.MOPPING
|
|
return DreameVacuumCleaningMode.SWEEPING_AND_MOPPING
|
|
else:
|
|
if values[0] == 2:
|
|
return DreameVacuumCleaningMode.SWEEPING
|
|
if values[0] == 0:
|
|
return DreameVacuumCleaningMode.SWEEPING_AND_MOPPING
|
|
value = values[0]
|
|
elif self.mop_pad_lifting_available:
|
|
if value == 2:
|
|
return DreameVacuumCleaningMode.SWEEPING
|
|
if value == 0:
|
|
return DreameVacuumCleaningMode.SWEEPING_AND_MOPPING
|
|
|
|
if value is None:
|
|
return DreameVacuumCleaningMode.SWEEPING_AND_MOPPING if self.water_tank_or_mop_installed else DreameVacuumCleaningMode.SWEEPING
|
|
|
|
if value in DreameVacuumCleaningMode._value2member_map_:
|
|
return DreameVacuumCleaningMode(value)
|
|
_LOGGER.debug("CLEANING_MODE not supported: %s", value)
|
|
return DreameVacuumCleaningMode.UNKNOWN
|
|
|
|
@property
|
|
def cleaning_mode_name(self) -> str:
|
|
"""Return cleaning mode as string for translation."""
|
|
return CLEANING_MODE_CODE_TO_NAME.get(self.cleaning_mode, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def status(self) -> DreameVacuumStatus:
|
|
"""Return status of the device."""
|
|
value = self._get_property(DreameVacuumProperty.STATUS)
|
|
if value is not None and value in DreameVacuumStatus._value2member_map_:
|
|
return DreameVacuumStatus(value)
|
|
_LOGGER.debug("STATUS not supported: %s", value)
|
|
return DreameVacuumStatus.UNKNOWN
|
|
|
|
@property
|
|
def status_name(self) -> str:
|
|
"""Return status as string for translation."""
|
|
return STATUS_CODE_TO_NAME.get(self.status, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def task_status(self) -> DreameVacuumTaskStatus:
|
|
"""Return task status of the device."""
|
|
value = self._get_property(DreameVacuumProperty.TASK_STATUS)
|
|
if value is not None and value in DreameVacuumTaskStatus._value2member_map_:
|
|
return DreameVacuumTaskStatus(value)
|
|
_LOGGER.debug("TASK_STATUS not supported: %s", value)
|
|
return DreameVacuumTaskStatus.UNKNOWN
|
|
|
|
@property
|
|
def task_status_name(self) -> str:
|
|
"""Return task status as string for translation."""
|
|
return TASK_STATUS_CODE_TO_NAME.get(self.task_status, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def water_tank(self) -> DreameVacuumWaterTank:
|
|
"""Return water tank of the device."""
|
|
value = self._get_property(DreameVacuumProperty.WATER_TANK)
|
|
if value is not None:
|
|
if value == 3:
|
|
return DreameVacuumWaterTank.INSTALLED
|
|
if value == 2:
|
|
return DreameVacuumWaterTank.MOP_INSTALLED
|
|
|
|
if value in DreameVacuumWaterTank._value2member_map_:
|
|
return DreameVacuumWaterTank(value)
|
|
_LOGGER.debug("WATER_TANK not supported: %s", value)
|
|
return DreameVacuumWaterTank.UNKNOWN
|
|
|
|
@property
|
|
def water_tank_name(self) -> str:
|
|
"""Return water tank as string for translation."""
|
|
return WATER_TANK_CODE_TO_NAME.get(self.water_tank, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def charging_status(self) -> DreameVacuumChargingStatus:
|
|
"""Return charging status of the device."""
|
|
value = self._get_property(DreameVacuumProperty.CHARGING_STATUS)
|
|
if value is not None and value in DreameVacuumChargingStatus._value2member_map_:
|
|
value = DreameVacuumChargingStatus(value)
|
|
# Charging status complete is not present on older firmwares
|
|
if (
|
|
value is DreameVacuumChargingStatus.CHARGING
|
|
and self.battery_level == 100
|
|
):
|
|
return DreameVacuumChargingStatus.CHARGING_COMPLETED
|
|
return value
|
|
_LOGGER.debug("CHARGING_STATUS not supported: %s", value)
|
|
return DreameVacuumChargingStatus.UNKNOWN
|
|
|
|
@property
|
|
def charging_status_name(self) -> str:
|
|
"""Return charging status as string for translation."""
|
|
return CHARGING_STATUS_CODE_TO_NAME.get(self.charging_status, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def auto_empty_status(self) -> DreameVacuumAutoEmptyStatus:
|
|
"""Return auto empty status of the device."""
|
|
value = self._get_property(DreameVacuumProperty.AUTO_EMPTY_STATUS)
|
|
if (
|
|
value is not None
|
|
and value in DreameVacuumAutoEmptyStatus._value2member_map_
|
|
):
|
|
return DreameVacuumAutoEmptyStatus(value)
|
|
_LOGGER.debug("AUTO_EMPTY_STATUS not supported: %s", value)
|
|
return DreameVacuumAutoEmptyStatus.UNKNOWN
|
|
|
|
@property
|
|
def auto_empty_status_name(self) -> str:
|
|
"""Return auto empty status as string for translation."""
|
|
return AUTO_EMPTY_STATUS_TO_NAME.get(self.auto_empty_status, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def relocation_status(self) -> DreameVacuumRelocationStatus:
|
|
"""Return relocation status of the device."""
|
|
value = self._get_property(DreameVacuumProperty.RELOCATION_STATUS)
|
|
if (
|
|
value is not None
|
|
and value in DreameVacuumRelocationStatus._value2member_map_
|
|
):
|
|
return DreameVacuumRelocationStatus(value)
|
|
_LOGGER.debug("RELOCATION_STATUS not supported: %s", value)
|
|
return DreameVacuumRelocationStatus.UNKNOWN
|
|
|
|
@property
|
|
def relocation_status_name(self) -> str:
|
|
"""Return relocation status as string for translation."""
|
|
return RELOCATION_STATUS_CODE_TO_NAME.get(self.relocation_status, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def self_wash_base_status(self) -> DreameVacuumSelfWashBaseStatus:
|
|
"""Return self-wash base status of the device."""
|
|
value = self._get_property(DreameVacuumProperty.SELF_WASH_BASE_STATUS)
|
|
if (
|
|
value is not None
|
|
and value in DreameVacuumSelfWashBaseStatus._value2member_map_
|
|
):
|
|
return DreameVacuumSelfWashBaseStatus(value)
|
|
_LOGGER.debug("SELF_WASH_BASE_STATUS not supported: %s", value)
|
|
return DreameVacuumSelfWashBaseStatus.UNKNOWN
|
|
|
|
@property
|
|
def self_wash_base_status_name(self) -> str:
|
|
"""Return self-wash base status as string for translation."""
|
|
return SELF_WASH_BASE_STATUS_TO_NAME.get(self.self_wash_base_status, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def dust_collection(self) -> DreameVacuumDustCollection:
|
|
value = self._get_property(DreameVacuumProperty.DUST_COLLECTION)
|
|
if value is not None and value in DreameVacuumDustCollection._value2member_map_:
|
|
return DreameVacuumDustCollection(value)
|
|
_LOGGER.debug("DUST_COLLECTION not supported: %s", value)
|
|
return DreameVacuumDustCollection.UNKNOWN
|
|
|
|
@property
|
|
def dust_collection_name(self) -> str:
|
|
"""Return dust collection as string for translation."""
|
|
return DUST_COLLECTION_TO_NAME.get(self.dust_collection, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def carpet_sensitivity(self) -> DreameVacuumCarpetSensitivity:
|
|
"""Return carpet sensitivity of the device."""
|
|
value = self._get_property(DreameVacuumProperty.CARPET_SENSITIVITY)
|
|
if (
|
|
value is not None
|
|
and value in DreameVacuumCarpetSensitivity._value2member_map_
|
|
):
|
|
return DreameVacuumCarpetSensitivity(value)
|
|
_LOGGER.debug("CARPET_SENSITIVITY not supported: %s", value)
|
|
return DreameVacuumCarpetSensitivity.UNKNOWN
|
|
|
|
@property
|
|
def carpet_sensitivity_name(self) -> str:
|
|
"""Return carpet sensitivity as string for translation."""
|
|
return CARPET_SENSITIVITY_CODE_TO_NAME.get(
|
|
self.carpet_sensitivity, STATE_UNKNOWN
|
|
)
|
|
|
|
@property
|
|
def state(self) -> DreameVacuumState:
|
|
"""Return state of the device."""
|
|
value = self._get_property(DreameVacuumProperty.STATE)
|
|
if value is not None and value in DreameVacuumState._value2member_map_:
|
|
vacuum_state = DreameVacuumState(value)
|
|
|
|
## Determine state as implemented on the app
|
|
if vacuum_state is DreameVacuumState.IDLE:
|
|
if (
|
|
self.started
|
|
or self.cleaning_paused
|
|
or self.fast_mapping_paused
|
|
):
|
|
return DreameVacuumState.PAUSED
|
|
elif self.docked:
|
|
## This is for compatibility with various lovelace vacuum cards
|
|
## Device will report idle when charging is completed and vacuum card will display return to dock icon even when robot is docked
|
|
if self.washing:
|
|
return DreameVacuumState.WASHING
|
|
if self.drying:
|
|
return DreameVacuumState.DRYING
|
|
if self.charging:
|
|
return DreameVacuumState.CHARGING
|
|
if self.charging_status is DreameVacuumChargingStatus.CHARGING_COMPLETED:
|
|
return DreameVacuumState.CHARGING_COMPLETED
|
|
return vacuum_state
|
|
_LOGGER.debug("STATE not supported: %s", value)
|
|
return DreameVacuumState.UNKNOWN
|
|
|
|
@property
|
|
def state_name(self) -> str:
|
|
"""Return state as string for translation."""
|
|
return STATE_CODE_TO_STATE.get(self.state, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def self_clean_area(self) -> DreameVacuumSelfCleanArea:
|
|
"""Return self-clean area of the device."""
|
|
if self.self_wash_base_available:
|
|
values = DreameVacuumDevice.split_group_value(self._get_property(DreameVacuumProperty.CLEANING_MODE))
|
|
if values and len(values) == 3:
|
|
value = values[1]
|
|
if (
|
|
value is not None
|
|
and value in DreameVacuumSelfCleanArea._value2member_map_
|
|
):
|
|
return DreameVacuumSelfCleanArea(value)
|
|
_LOGGER.debug("SELF_CLEAN_AREA not supported: %s", value)
|
|
return DreameVacuumSelfCleanArea.UNKNOWN
|
|
|
|
@property
|
|
def self_clean_area_name(self) -> str:
|
|
"""Return self-clean area as string for translation."""
|
|
return SELF_AREA_CLEAN_TO_NAME.get(self.self_clean_area, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def mop_wash_level(self) -> DreameVacuumSelfCleanArea:
|
|
"""Return mop wash level of the device."""
|
|
if self.self_wash_base_available:
|
|
value = self._get_property(DreameVacuumProperty.MOP_WASH_LEVEL)
|
|
if (
|
|
value is not None
|
|
and value in DreameVacuumMopWashLevel._value2member_map_
|
|
):
|
|
return DreameVacuumMopWashLevel(value)
|
|
_LOGGER.debug("MOP_WASH_LEVEL not supported: %s", value)
|
|
return DreameVacuumMopWashLevel.UNKNOWN
|
|
|
|
@property
|
|
def mop_wash_level_name(self) -> str:
|
|
"""Return mop wash level as string for translation."""
|
|
return MOP_WASH_LEVEL_TO_NAME.get(self.mop_wash_level, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def mopping_type_name(self) -> str:
|
|
"""Return moping type as string for translation."""
|
|
if (
|
|
self.mopping_type is not None
|
|
and self.mopping_type in DreameVacuumMoppingType._value2member_map_
|
|
):
|
|
return MOPPING_TYPE_TO_NAME.get(DreameVacuumMoppingType(self.mopping_type), STATE_UNKNOWN)
|
|
return STATE_UNKNOWN
|
|
|
|
@property
|
|
def error(self) -> DreameVacuumErrorCode:
|
|
"""Return error of the device."""
|
|
value = self._get_property(DreameVacuumProperty.ERROR)
|
|
if value is not None and value in DreameVacuumErrorCode._value2member_map_:
|
|
return DreameVacuumErrorCode(value)
|
|
_LOGGER.debug("ERROR_CODE not supported: %s", value)
|
|
return DreameVacuumErrorCode.UNKNOWN
|
|
|
|
@property
|
|
def error_name(self) -> str:
|
|
"""Return error as string for translation."""
|
|
return ERROR_CODE_TO_ERROR_NAME.get(self.error, STATE_UNKNOWN)
|
|
|
|
@property
|
|
def error_description(self) -> str:
|
|
"""Return error description of the device."""
|
|
return ERROR_CODE_TO_ERROR_DESCRIPTION.get(self.error, [STATE_UNKNOWN, ""])
|
|
|
|
@property
|
|
def error_image(self) -> str:
|
|
"""Return error image of the device as base64 string."""
|
|
if not self.has_error:
|
|
return None
|
|
return ERROR_IMAGE.get(ERROR_CODE_TO_IMAGE_INDEX.get(self.error, 19))
|
|
|
|
@property
|
|
def robot_status(self) -> int: # TODO: Convert to enum
|
|
"""Device status for robot icon rendering."""
|
|
if self.running and not self.returning and not self.fast_mapping:
|
|
return 1
|
|
elif self.self_wash_base_available and (self.washing or self.drying or self.washing_paused):
|
|
if self.has_error or self.has_warning:
|
|
return 6
|
|
return 7
|
|
elif self.charging:
|
|
return 2
|
|
elif self.has_error or self.has_warning:
|
|
if self.sleeping:
|
|
return 5
|
|
else:
|
|
return 3
|
|
elif self.sleeping:
|
|
return 4
|
|
return 0
|
|
|
|
@property
|
|
def has_error(self) -> bool:
|
|
"""Returns true when an error is present."""
|
|
error = self.error
|
|
return bool(
|
|
error.value > 0
|
|
and not self.has_warning
|
|
and error != DreameVacuumErrorCode.BATTERY_LOW
|
|
)
|
|
|
|
@property
|
|
def has_warning(self) -> bool:
|
|
"""Returns true when a warning is present and available for dismiss."""
|
|
error = self.error
|
|
return bool(
|
|
error.value > 0
|
|
and (
|
|
error == DreameVacuumErrorCode.REMOVE_MOP
|
|
or error == DreameVacuumErrorCode.MOP_REMOVED_2
|
|
or error == DreameVacuumErrorCode.CLEAN_MOP_PAD
|
|
or error == DreameVacuumErrorCode.BLOCKED
|
|
or error == DreameVacuumErrorCode.WATER_TANK_DRY
|
|
or error == DreameVacuumErrorCode.MOP_PAD_STOP_ROTATE
|
|
or error == DreameVacuumErrorCode.MOP_PAD_STOP_ROTATE_2
|
|
)
|
|
)
|
|
|
|
@property
|
|
def dust_collection_available(self) -> bool:
|
|
"""Returns true when robot is docked and can start auto emptying."""
|
|
return bool(self._get_property(DreameVacuumProperty.DUST_COLLECTION))
|
|
|
|
@property
|
|
def self_clean(self) -> bool:
|
|
return bool(self._get_property(DreameVacuumProperty.SELF_CLEAN) == 1)
|
|
|
|
@property
|
|
def scheduled_clean(self) -> bool:
|
|
return bool(self._get_property(DreameVacuumProperty.SCHEDULED_CLEAN) == 1)
|
|
|
|
@property
|
|
def auto_mount(self) -> bool:
|
|
return bool(self._get_property(DreameVacuumProperty.AUTO_MOUNT_MOP) == 1)
|
|
|
|
@property
|
|
def dnd_remaining(self) -> bool:
|
|
"""Returns remaining seconds to DND period to end."""
|
|
if self.dnd_enabled:
|
|
dnd_start = self.dnd_start
|
|
dnd_end = self.dnd_end
|
|
if dnd_start and dnd_end:
|
|
end_time = dnd_end.split(":")
|
|
if len(end_time) == 2:
|
|
now = datetime.now()
|
|
hour = now.hour
|
|
minute = now.minute
|
|
if minute < 10:
|
|
minute = f"0{minute}"
|
|
|
|
time = int(f"{hour}{minute}")
|
|
start = int(dnd_start.replace(":", ""))
|
|
end = int(dnd_end.replace(":", ""))
|
|
current_seconds = hour * 3600 + int(minute) * 60
|
|
end_seconds = int(end_time[0]) * \
|
|
3600 + int(end_time[1]) * 60
|
|
|
|
if (
|
|
start < end
|
|
and start < time
|
|
and time < end
|
|
or end < start
|
|
and (2400 > time and time > start or end > time and time > 0)
|
|
or time == start
|
|
or time == end
|
|
):
|
|
return (
|
|
(end_seconds + 86400 - current_seconds)
|
|
if current_seconds > end_seconds
|
|
else (end_seconds - current_seconds)
|
|
)
|
|
return 0
|
|
return None
|
|
|
|
@property
|
|
def water_tank_or_mop_installed(self) -> bool:
|
|
"""Returns true when water tank or additional mop is installed to the device."""
|
|
water_tank = self.water_tank
|
|
return bool(water_tank is DreameVacuumWaterTank.INSTALLED or water_tank is DreameVacuumWaterTank.MOP_INSTALLED)
|
|
|
|
@property
|
|
def located(self) -> bool:
|
|
"""Returns true when robot knows its position on current map."""
|
|
relocation_status = self.relocation_status
|
|
return bool(
|
|
relocation_status is DreameVacuumRelocationStatus.LOCATED
|
|
or relocation_status is DreameVacuumRelocationStatus.UNKNOWN
|
|
)
|
|
|
|
@property
|
|
def sweeping(self) -> bool:
|
|
"""Returns true when cleaning mode is sweeping therefore cannot set its water volume."""
|
|
cleaning_mode = self.cleaning_mode
|
|
return bool(cleaning_mode is not DreameVacuumCleaningMode.MOPPING and cleaning_mode is not DreameVacuumCleaningMode.SWEEPING_AND_MOPPING)
|
|
|
|
@property
|
|
def mopping(self) -> bool:
|
|
"""Returns true when cleaning mode is mopping therefore cannot set its suction level."""
|
|
return bool(self.cleaning_mode is DreameVacuumCleaningMode.MOPPING)
|
|
|
|
@property
|
|
def zone_cleaning(self) -> bool:
|
|
"""Returns true when device is currently performing a zone cleaning task."""
|
|
task_status = self.task_status
|
|
return bool(
|
|
self._device_connected
|
|
and self.started
|
|
and (
|
|
task_status is DreameVacuumTaskStatus.ZONE_CLEANING
|
|
or task_status is DreameVacuumTaskStatus.ZONE_CLEANING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.ZONE_MOPPING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.ZONE_DOCKING_PAUSED
|
|
)
|
|
)
|
|
|
|
@property
|
|
def spot_cleaning(self) -> bool:
|
|
"""Returns true when device is currently performing a spot cleaning task."""
|
|
task_status = self.task_status
|
|
return bool(
|
|
self._device_connected
|
|
and self.started
|
|
and (
|
|
task_status is DreameVacuumTaskStatus.SPOT_CLEANING
|
|
or task_status is DreameVacuumTaskStatus.SPOT_CLEANING_PAUSED
|
|
or self.status is DreameVacuumStatus.SPOT_CLEANING
|
|
)
|
|
)
|
|
|
|
@property
|
|
def segment_cleaning(self) -> bool:
|
|
"""Returns true when device is currently performing a custom segment cleaning task."""
|
|
task_status = self.task_status
|
|
return bool(
|
|
self._device_connected
|
|
and self.started
|
|
and (
|
|
task_status is DreameVacuumTaskStatus.SEGMENT_CLEANING
|
|
or task_status is DreameVacuumTaskStatus.SEGMENT_CLEANING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.SEGMENT_MOPPING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.SEGMENT_DOCKING_PAUSED
|
|
)
|
|
)
|
|
|
|
@property
|
|
def auto_cleaning(self) -> bool:
|
|
"""Returns true when device is currently performing a complete map cleaning task."""
|
|
task_status = self.task_status
|
|
return bool(
|
|
self._device_connected
|
|
and self.started
|
|
and (
|
|
task_status is DreameVacuumTaskStatus.AUTO_CLEANING
|
|
or task_status is DreameVacuumTaskStatus.AUTO_CLEANING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.AUTO_MOPPING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.AUTO_DOCKING_PAUSED
|
|
)
|
|
)
|
|
|
|
@property
|
|
def fast_mapping(self) -> bool:
|
|
"""Returns true when device is creating a new map."""
|
|
return bool(
|
|
self._device_connected
|
|
and (
|
|
self.task_status is DreameVacuumTaskStatus.FAST_MAPPING
|
|
or self.status is DreameVacuumStatus.FAST_MAPPING
|
|
or self.fast_mapping_paused
|
|
)
|
|
)
|
|
|
|
@property
|
|
def fast_mapping_paused(self) -> bool:
|
|
"""Returns true when creating a new map paused by user.
|
|
Used for resuming fast cleaning on start because standard start action can not be used for resuming fast mapping."""
|
|
|
|
state = self._get_property(DreameVacuumProperty.STATE)
|
|
task_status = self.task_status
|
|
return bool(
|
|
(
|
|
task_status == DreameVacuumTaskStatus.FAST_MAPPING
|
|
or task_status == DreameVacuumTaskStatus.MAP_CLEANING_PAUSED
|
|
)
|
|
and (
|
|
state == DreameVacuumState.PAUSED
|
|
or state == DreameVacuumState.ERROR
|
|
or state == DreameVacuumState.IDLE
|
|
)
|
|
)
|
|
|
|
@property
|
|
def carpet_avoidance(self) -> bool:
|
|
"""Returns true when carpet avoidance feature is enabled."""
|
|
value = self._get_property(DreameVacuumProperty.CARPET_AVOIDANCE)
|
|
return bool(value == 1)
|
|
|
|
@property
|
|
def auto_add_detergent(self) -> bool:
|
|
"""Returns true when auto-add detergent feature is enabled."""
|
|
value = self._get_property(DreameVacuumProperty.AUTO_ADD_DETERGENT)
|
|
return bool(value == 1 or value == 3)
|
|
|
|
@property
|
|
def cleaning_paused(self) -> bool:
|
|
"""Returns true when device battery is too low for resuming its task and needs to be charged before continuing."""
|
|
return bool(self._get_property(DreameVacuumProperty.CLEANING_PAUSED))
|
|
|
|
@property
|
|
def charging(self) -> bool:
|
|
"""Returns true when device is currently charging."""
|
|
return bool(self.charging_status is DreameVacuumChargingStatus.CHARGING)
|
|
|
|
@property
|
|
def docked(self) -> bool:
|
|
"""Returns true when device is docked."""
|
|
return bool(
|
|
self.charging
|
|
or self.charging_status is DreameVacuumChargingStatus.CHARGING_COMPLETED
|
|
or self.washing
|
|
or self.drying
|
|
or self.washing_paused
|
|
)
|
|
|
|
@property
|
|
def sleeping(self) -> bool:
|
|
"""Returns true when device is sleeping."""
|
|
return bool(self.status is DreameVacuumStatus.SLEEPING)
|
|
|
|
@property
|
|
def returning_paused(self) -> bool:
|
|
"""Returns true when returning to dock is paused."""
|
|
task_status = self.task_status
|
|
return bool(
|
|
self._device_connected
|
|
and task_status is DreameVacuumTaskStatus.DOCKING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.AUTO_DOCKING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.SEGMENT_DOCKING_PAUSED
|
|
or task_status is DreameVacuumTaskStatus.ZONE_DOCKING_PAUSED
|
|
)
|
|
|
|
@property
|
|
def returning(self) -> bool:
|
|
"""Returns true when returning to dock for charging or washing."""
|
|
return bool(
|
|
self._device_connected
|
|
and (
|
|
self.status is DreameVacuumStatus.BACK_HOME
|
|
or self.returning_to_wash
|
|
)
|
|
)
|
|
|
|
@property
|
|
def started(self) -> bool:
|
|
"""Returns true when device has an active task.
|
|
Used for preventing updates on settings that relates to currently performing task."""
|
|
return bool(
|
|
self.task_status != DreameVacuumTaskStatus.COMPLETED or self.cleaning_paused
|
|
)
|
|
|
|
@property
|
|
def paused(self) -> bool:
|
|
"""Returns true when device has an active paused task."""
|
|
status = self.status
|
|
return bool(
|
|
self.started
|
|
and (
|
|
status is DreameVacuumStatus.PAUSED
|
|
or status is DreameVacuumStatus.SLEEPING
|
|
or status is DreameVacuumStatus.IDLE
|
|
or status is DreameVacuumStatus.STANDBY
|
|
)
|
|
or self.cleaning_paused
|
|
)
|
|
|
|
@property
|
|
def active(self) -> bool:
|
|
"""Returns true when device is moving or not sleeping."""
|
|
return self.status is DreameVacuumStatus.STANDBY or self.running
|
|
|
|
@property
|
|
def running(self) -> bool:
|
|
"""Returns true when device is moving."""
|
|
status = self.status
|
|
return bool(
|
|
not self.docked
|
|
and (
|
|
status is DreameVacuumStatus.CLEANING
|
|
or status is DreameVacuumStatus.BACK_HOME
|
|
or status is DreameVacuumStatus.PART_CLEANING
|
|
or status is DreameVacuumStatus.FOLLOW_WALL
|
|
or status is DreameVacuumStatus.REMOTE_CONTROL
|
|
or status is DreameVacuumStatus.SEGMENT_CLEANING
|
|
or status is DreameVacuumStatus.ZONE_CLEANING
|
|
or status is DreameVacuumStatus.SPOT_CLEANING
|
|
or status is DreameVacuumStatus.PART_CLEANING
|
|
or status is DreameVacuumStatus.FAST_MAPPING
|
|
or status is DreameVacuumStatus.MONITOR_CRUISE
|
|
or status is DreameVacuumStatus.MONITOR_SPOT
|
|
or status is DreameVacuumStatus.SUMMON_CLEAN
|
|
)
|
|
)
|
|
|
|
@property
|
|
def auto_emptying(self) -> bool:
|
|
"""Returns true when device is auto emptying."""
|
|
return bool(self.auto_empty_status is DreameVacuumAutoEmptyStatus.ACTIVE)
|
|
|
|
@property
|
|
def auto_emptying_not_performed(self) -> bool:
|
|
"""Returns true when auto emptying is not performed due to DND settings."""
|
|
return bool(self.auto_empty_status is DreameVacuumAutoEmptyStatus.NOT_PERFORMED)
|
|
|
|
@property
|
|
def customized_cleaning(self) -> bool:
|
|
"""Returns true when customized cleaning feature is enabled."""
|
|
return bool(
|
|
self._get_property(DreameVacuumProperty.CUSTOMIZED_CLEANING)
|
|
and self.has_saved_map
|
|
)
|
|
|
|
@property
|
|
def multi_map(self) -> bool:
|
|
"""Returns true when multi floor map feature is enabled."""
|
|
return bool(self._get_property(DreameVacuumProperty.MULTI_FLOOR_MAP))
|
|
|
|
@property
|
|
def last_cleaning_time(self) -> datetime | None:
|
|
if self._cleaning_history:
|
|
return self._last_cleaning_time
|
|
|
|
@property
|
|
def cleaning_history(self) -> dict[str, Any] | None:
|
|
"""Returns the cleaning history list as dict."""
|
|
if self._cleaning_history:
|
|
list = {}
|
|
for history in self._cleaning_history:
|
|
date = time.strftime(
|
|
"%Y-%m-%d %H:%M:%S", time.localtime(
|
|
history.date.timestamp())
|
|
)
|
|
list[date] = {
|
|
ATTR_CLEANING_TIME: f"{history.cleaning_time} min",
|
|
ATTR_CLEANED_AREA: f"{history.cleaned_area} m²",
|
|
}
|
|
if history.status is not None:
|
|
list[date][ATTR_STATUS] = (
|
|
STATUS_CODE_TO_NAME.get(history.status, STATE_UNKNOWN)
|
|
.replace("_", " ")
|
|
.capitalize()
|
|
)
|
|
if history.suction_level is not None:
|
|
list[date][ATTR_SUCTION_LEVEL] = (
|
|
SUCTION_LEVEL_CODE_TO_NAME.get(
|
|
history.suction_level, STATE_UNKNOWN)
|
|
.replace("_", " ")
|
|
.capitalize()
|
|
)
|
|
if history.completed is not None:
|
|
list[date][ATTR_COMPLETED] = history.completed
|
|
if history.water_tank is not None:
|
|
list[date][ATTR_WATER_TANK] = (
|
|
WATER_TANK_CODE_TO_NAME.get(
|
|
history.water_tank, STATE_UNKNOWN)
|
|
.replace("_", " ")
|
|
.capitalize()
|
|
)
|
|
return list
|
|
|
|
@property
|
|
def washing(self) -> bool:
|
|
"""Returns true the when device is currently performing mop washing."""
|
|
return bool(self.self_wash_base_available and (self.self_wash_base_status is DreameVacuumSelfWashBaseStatus.WASHING or self.self_wash_base_status is DreameVacuumSelfWashBaseStatus.CLEAN_ADD_WATER))
|
|
|
|
@property
|
|
def drying(self) -> bool:
|
|
"""Returns true the when device is currently performing mop drying."""
|
|
return bool(self.self_wash_base_available and self.self_wash_base_status is DreameVacuumSelfWashBaseStatus.DRYING)
|
|
|
|
@property
|
|
def washing_paused(self) -> bool:
|
|
"""Returns true when mop washing paused."""
|
|
return bool(self.self_wash_base_available and self.self_wash_base_status is DreameVacuumSelfWashBaseStatus.PAUSED)
|
|
|
|
@property
|
|
def returning_to_wash(self) -> bool:
|
|
"""Returns true when the device returning to self-wash base to wash or dry its mop."""
|
|
return bool(self.self_wash_base_available and self.self_wash_base_status is DreameVacuumSelfWashBaseStatus.RETURNING and (self.state == DreameVacuumState.RETURNING or self.state == DreameVacuumState.RETURNING_WASHING))
|
|
|
|
@property
|
|
def returning_to_wash_paused(self) -> bool:
|
|
"""Returns true when the device returning to self-wash base to wash or dry its mop."""
|
|
return bool(self.self_wash_base_available and self.self_wash_base_status is DreameVacuumSelfWashBaseStatus.RETURNING and self.state == DreameVacuumState.PAUSED)
|
|
|
|
@property
|
|
def washing_available(self) -> bool:
|
|
"""Returns true when device has a self-wash base and washing mop can be performed."""
|
|
return bool(
|
|
self.self_wash_base_available and
|
|
self.water_tank_or_mop_installed and
|
|
not
|
|
(
|
|
self.washing or
|
|
self.washing_paused or
|
|
self.returning_to_wash_paused or
|
|
self.returning_to_wash or
|
|
self.returning or
|
|
self.returning_paused or
|
|
self.cleaning_paused
|
|
)
|
|
)
|
|
|
|
@property
|
|
def drying_available(self) -> bool:
|
|
"""Returns true when device has a self-wash base and drying mop can be performed."""
|
|
return bool(
|
|
self.self_wash_base_available and
|
|
self.water_tank_or_mop_installed and
|
|
self.docked and
|
|
not (self.washing or self.washing_paused)
|
|
)
|
|
|
|
@property
|
|
def mapping_available(self) -> bool:
|
|
"""Returns true when creating a new map is possible."""
|
|
return bool(
|
|
not self.started
|
|
and not self.fast_mapping
|
|
and (
|
|
not self.map_available
|
|
or ((3 if self.multi_map else 1) > len(self.map_list))
|
|
)
|
|
)
|
|
|
|
@property
|
|
def main_brush_life(self) -> int:
|
|
"""Returns main brush remaining life in percent."""
|
|
return self._get_property(DreameVacuumProperty.MAIN_BRUSH_LEFT)
|
|
|
|
@property
|
|
def side_brush_life(self) -> int:
|
|
"""Returns side brush remaining life in percent."""
|
|
return self._get_property(DreameVacuumProperty.SIDE_BRUSH_LEFT)
|
|
|
|
@property
|
|
def filter_life(self) -> int:
|
|
"""Returns filter remaining life in percent."""
|
|
return self._get_property(DreameVacuumProperty.FILTER_LEFT)
|
|
|
|
@property
|
|
def sensor_dirty_life(self) -> int:
|
|
"""Returns sensor clean remaining life in percent."""
|
|
return self._get_property(DreameVacuumProperty.SENSOR_DIRTY_LEFT)
|
|
|
|
@property
|
|
def secondary_filter_life(self) -> int:
|
|
"""Returns secondary filter remaining life in percent."""
|
|
return self._get_property(DreameVacuumProperty.SECONDARY_FILTER_LEFT)
|
|
|
|
@property
|
|
def mop_life(self) -> int:
|
|
"""Returns mop remaining life in percent."""
|
|
return self._get_property(DreameVacuumProperty.MOP_PAD_LEFT)
|
|
|
|
@property
|
|
def silver_ion_life(self) -> int:
|
|
"""Returns silver-ion life in percent."""
|
|
return self._get_property(DreameVacuumProperty.SILVER_ION_LEFT)
|
|
|
|
@property
|
|
def detergent_life(self) -> int:
|
|
"""Returns detergent life in percent."""
|
|
return self._get_property(DreameVacuumProperty.DETERGENT_LEFT)
|
|
|
|
@property
|
|
def dnd_enabled(self) -> bool:
|
|
"""Returns DND is enabled."""
|
|
return bool(self._get_property(DreameVacuumProperty.DND))
|
|
|
|
@property
|
|
def dnd_start(self) -> str:
|
|
"""Returns DND start time."""
|
|
return self._get_property(DreameVacuumProperty.DND_START)
|
|
|
|
@property
|
|
def dnd_end(self) -> str:
|
|
"""Returns DND end time."""
|
|
return self._get_property(DreameVacuumProperty.DND_END)
|
|
|
|
@property
|
|
def custom_order(self) -> bool:
|
|
"""Returns true when custom cleaning sequence is set."""
|
|
segments = self.segments
|
|
if segments:
|
|
for v in segments.values():
|
|
if v.order:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def cleaning_sequence(self) -> list[int] | None:
|
|
"""Returns custom cleaning sequence list."""
|
|
segments = self.segments
|
|
if segments:
|
|
return list(sorted(segments, key=lambda segment_id: segments[segment_id].order if segments[segment_id].order else 99)) if self.custom_order else None
|
|
return [] if self.custom_order else None
|
|
|
|
@property
|
|
def map_available(self) -> bool:
|
|
"""Returns true when mapping feature is available."""
|
|
return bool(self._map_manager is not None)
|
|
|
|
@property
|
|
def has_saved_map(self) -> bool:
|
|
"""Returns true when device has saved map and knowns its location on saved map."""
|
|
if not self.map_available:
|
|
return True
|
|
|
|
current_map = self.current_map
|
|
return bool(
|
|
current_map is not None
|
|
and current_map.saved_map_status == 2
|
|
and not self.has_temporary_map
|
|
and not self.has_new_map
|
|
and not current_map.empty_map
|
|
)
|
|
|
|
@property
|
|
def has_temporary_map(self) -> bool:
|
|
"""Returns true when device cannot store the newly created map and waits prompt for restoring or discarding it."""
|
|
if not self.map_available:
|
|
return False
|
|
|
|
current_map = self.current_map
|
|
return bool(
|
|
current_map is not None
|
|
and current_map.temporary_map
|
|
and not current_map.empty_map
|
|
)
|
|
|
|
@property
|
|
def has_new_map(self) -> bool:
|
|
"""Returns true when fast mapping from empty map."""
|
|
if not self.map_available:
|
|
return False
|
|
|
|
current_map = self.current_map
|
|
return bool(
|
|
current_map is not None
|
|
and not current_map.temporary_map
|
|
and not current_map.empty_map
|
|
and current_map.new_map
|
|
)
|
|
|
|
@property
|
|
def selected_map(self) -> MapData | None:
|
|
"""Return the selected map data"""
|
|
if self.map_available and not self.has_temporary_map and not self.has_new_map:
|
|
return self._map_manager.selected_map
|
|
|
|
@property
|
|
def current_map(self) -> MapData | None:
|
|
"""Return the current map data"""
|
|
if self.map_available:
|
|
return self._map_manager.get_map()
|
|
|
|
@property
|
|
def map_list(self) -> list[int] | None:
|
|
"""Return the saved map id list if multi floor map is enabled"""
|
|
if self.map_available:
|
|
if self.multi_map:
|
|
return self._map_manager.map_list
|
|
|
|
selected_map = self._map_manager.selected_map
|
|
if selected_map:
|
|
return [selected_map.map_id]
|
|
return []
|
|
|
|
@property
|
|
def map_data_list(self) -> dict[int, MapData] | None:
|
|
"""Return the saved map data list if multi floor map is enabled"""
|
|
if self.map_available:
|
|
if self.multi_map:
|
|
return self._map_manager.map_data_list
|
|
selected_map = self.selected_map
|
|
if selected_map:
|
|
return {selected_map.map_id: selected_map}
|
|
return {}
|
|
|
|
@property
|
|
def segments(self) -> dict[int, Segment] | None:
|
|
"""Return the segments of current map"""
|
|
current_map = self.current_map
|
|
if current_map and current_map.segments and not current_map.empty_map:
|
|
return current_map.segments
|
|
return {}
|
|
|
|
@property
|
|
def current_room(self) -> Segment | None:
|
|
"""Return the segment that device is currently on"""
|
|
if self.lidar_navigation:
|
|
current_map = self.current_map
|
|
if (
|
|
current_map
|
|
and current_map.segments
|
|
and current_map.robot_segment
|
|
and not current_map.empty_map
|
|
):
|
|
return current_map.segments[current_map.robot_segment]
|
|
|
|
@property
|
|
def active_segments(self) -> list[int] | None:
|
|
map_data = self.current_map
|
|
if map_data and self.started and not self.fast_mapping:
|
|
if self.segment_cleaning:
|
|
if map_data.active_segments:
|
|
return map_data.active_segments
|
|
elif not self.zone_cleaning and not self.spot_cleaning and map_data.segments and not self.docked and not self.returning and not self.returning_paused:
|
|
return list(map_data.segments.keys())
|
|
return []
|
|
|
|
@property
|
|
def job(self) -> dict[str, Any] | None:
|
|
attributes = {
|
|
ATTR_CLEANING_MODE: self.cleaning_mode.name,
|
|
ATTR_STATUS: self.status.name
|
|
}
|
|
attributes[ATTR_WATER_TANK if not self.self_wash_base_available else ATTR_MOP_PAD] = self.water_tank_or_mop_installed
|
|
|
|
if self._device.cleanup_completed:
|
|
attributes.update({
|
|
ATTR_CLEANED_AREA: self._get_property(DreameVacuumProperty.CLEANED_AREA),
|
|
ATTR_CLEANING_TIME: self._get_property(DreameVacuumProperty.CLEANING_TIME),
|
|
ATTR_COMPLETED: True,
|
|
})
|
|
else:
|
|
attributes[ATTR_COMPLETED] = False
|
|
|
|
map_data = self.current_map
|
|
if map_data:
|
|
if map_data.active_segments is not None:
|
|
attributes[ATTR_ACTIVE_SEGMENTS] = map_data.active_segments
|
|
elif map_data.active_areas is not None:
|
|
attributes[ATTR_ACTIVE_AREAS] = map_data.active_areas
|
|
elif map_data.active_points is not None:
|
|
attributes[ATTR_ACTIVE_POINTS] = map_data.active_points
|
|
return attributes
|
|
|
|
@property
|
|
def attributes(self) -> dict[str, Any] | None:
|
|
"""Return the attributes of the device."""
|
|
properties = [
|
|
DreameVacuumProperty.CLEANING_MODE,
|
|
DreameVacuumProperty.TIGHT_MOPPING,
|
|
DreameVacuumProperty.ERROR,
|
|
DreameVacuumProperty.CLEANING_TIME,
|
|
DreameVacuumProperty.CLEANED_AREA,
|
|
DreameVacuumProperty.VOICE_PACKET_ID,
|
|
DreameVacuumProperty.TIMEZONE,
|
|
DreameVacuumProperty.MAIN_BRUSH_TIME_LEFT,
|
|
DreameVacuumProperty.MAIN_BRUSH_LEFT,
|
|
DreameVacuumProperty.SIDE_BRUSH_TIME_LEFT,
|
|
DreameVacuumProperty.SIDE_BRUSH_LEFT,
|
|
DreameVacuumProperty.FILTER_LEFT,
|
|
DreameVacuumProperty.FILTER_TIME_LEFT,
|
|
DreameVacuumProperty.SENSOR_DIRTY_LEFT,
|
|
DreameVacuumProperty.SENSOR_DIRTY_TIME_LEFT,
|
|
DreameVacuumProperty.SECONDARY_FILTER_LEFT,
|
|
DreameVacuumProperty.SECONDARY_FILTER_TIME_LEFT,
|
|
DreameVacuumProperty.MOP_PAD_LEFT,
|
|
DreameVacuumProperty.MOP_PAD_TIME_LEFT,
|
|
DreameVacuumProperty.SILVER_ION_LEFT,
|
|
DreameVacuumProperty.SILVER_ION_TIME_LEFT,
|
|
DreameVacuumProperty.DETERGENT_LEFT,
|
|
DreameVacuumProperty.DETERGENT_TIME_LEFT,
|
|
DreameVacuumProperty.TOTAL_CLEANED_AREA,
|
|
DreameVacuumProperty.TOTAL_CLEANING_TIME,
|
|
DreameVacuumProperty.CLEANING_COUNT,
|
|
DreameVacuumProperty.DND_START,
|
|
DreameVacuumProperty.DND_END,
|
|
DreameVacuumProperty.CUSTOMIZED_CLEANING,
|
|
DreameVacuumProperty.SERIAL_NUMBER,
|
|
]
|
|
|
|
attributes = {}
|
|
if not self.self_wash_base_available:
|
|
attributes[ATTR_WATER_TANK] = self.water_tank_or_mop_installed
|
|
properties.append(DreameVacuumProperty.WATER_VOLUME)
|
|
else:
|
|
attributes[ATTR_MOP_PAD] = self.water_tank_or_mop_installed
|
|
if self.started and (self.customized_cleaning and not (self.zone_cleaning or self.spot_cleaning)):
|
|
attributes[ATTR_MOP_PAD_HUMIDITY] = STATE_UNAVAILABLE.capitalize()
|
|
attributes[f"{ATTR_MOP_PAD_HUMIDITY}_list"] = []
|
|
else:
|
|
attributes[ATTR_MOP_PAD_HUMIDITY] = self.mop_pad_humidity_name.replace("_", " ").capitalize()
|
|
attributes[f"{ATTR_MOP_PAD_HUMIDITY}_list"] = [v.replace("_", " ").capitalize() for v in self.mop_pad_humidity_list.keys()]
|
|
|
|
for prop in properties:
|
|
value = self._get_property(prop)
|
|
if value is not None:
|
|
prop_name = PROPERTY_TO_NAME.get(prop)
|
|
if prop_name:
|
|
prop_name = prop_name[0]
|
|
else:
|
|
prop_name = prop.name.lower()
|
|
|
|
if prop is DreameVacuumProperty.ERROR:
|
|
value = self.error_name.replace("_", " ").capitalize()
|
|
elif prop is DreameVacuumProperty.WATER_VOLUME:
|
|
if self.started and (self.customized_cleaning and not (self.zone_cleaning or self.spot_cleaning)):
|
|
value = STATE_UNAVAILABLE.capitalize()
|
|
attributes[f"{prop_name}_list"] = []
|
|
else:
|
|
value = self.water_volume_name.capitalize()
|
|
attributes[f"{prop_name}_list"] = [v.capitalize() for v in self.water_volume_list.keys()]
|
|
elif prop is DreameVacuumProperty.CLEANING_MODE:
|
|
value = self.cleaning_mode_name.replace("_", " ").capitalize()
|
|
elif prop is DreameVacuumProperty.CUSTOMIZED_CLEANING:
|
|
value = self.customized_cleaning and not self.zone_cleaning and not self.spot_cleaning
|
|
elif prop is DreameVacuumProperty.TIGHT_MOPPING:
|
|
value = "on" if value == 1 else "off"
|
|
attributes[prop_name] = value
|
|
|
|
attributes[ATTR_CLEANING_SEQUENCE] = self.cleaning_sequence
|
|
attributes[ATTR_CHARGING] = self.docked
|
|
attributes[ATTR_STARTED] = self.started
|
|
attributes[ATTR_PAUSED] = self.paused
|
|
attributes[ATTR_RUNNING] = self.running
|
|
attributes[ATTR_RETURNING_PAUSED] = self.returning_paused
|
|
attributes[ATTR_RETURNING] = self.returning
|
|
attributes[ATTR_MAPPING] = self.fast_mapping
|
|
|
|
if self.map_list:
|
|
attributes[ATTR_ACTIVE_SEGMENTS] = self.active_segments
|
|
if self.lidar_navigation:
|
|
attributes[ATTR_CURRENT_SEGMENT] = self.current_room.segment_id if self.current_room else 0
|
|
attributes[ATTR_SELECTED_MAP] = self.selected_map.map_name if self.selected_map else None
|
|
attributes[ATTR_ROOMS] = {}
|
|
for (k, v) in self.map_data_list.items():
|
|
attributes[ATTR_ROOMS][v.map_name] = [
|
|
{ATTR_ID: j, ATTR_NAME: s.name, ATTR_ICON: s.icon}
|
|
for (j, s) in sorted(v.segments.items())
|
|
]
|
|
|
|
return attributes
|
|
|
|
|
|
class DreameVacuumDeviceInfo:
|
|
"""Container of device information."""
|
|
|
|
def __init__(self, data):
|
|
self.data = data
|
|
|
|
def __repr__(self):
|
|
return "%s v%s (%s) @ %s - token: %s" % (
|
|
self.data["model"],
|
|
self.data["fw_ver"],
|
|
self.data["mac"],
|
|
self.network_interface["localIp"],
|
|
self.data["token"],
|
|
)
|
|
|
|
@property
|
|
def network_interface(self) -> str:
|
|
"""Information about network configuration."""
|
|
return self.data["netif"]
|
|
|
|
@property
|
|
def accesspoint(self) -> str:
|
|
"""Information about connected WLAN access point."""
|
|
return self.data["ap"]
|
|
|
|
@property
|
|
def model(self) -> Optional[str]:
|
|
"""Model string if available."""
|
|
if self.data["model"] is not None:
|
|
return self.data["model"]
|
|
return None
|
|
|
|
@property
|
|
def firmware_version(self) -> Optional[str]:
|
|
"""Firmware version if available."""
|
|
if self.data["fw_ver"] is not None:
|
|
return self.data["fw_ver"]
|
|
return None
|
|
|
|
@property
|
|
def version(self) -> Optional[int]:
|
|
"""Firmware version number if firmware version available."""
|
|
firmware_version = self.firmware_version
|
|
if firmware_version is not None:
|
|
firmware_version = firmware_version.split("_")
|
|
if len(firmware_version) == 2:
|
|
return int(firmware_version[1])
|
|
return None
|
|
|
|
@property
|
|
def hardware_version(self) -> Optional[str]:
|
|
"""Hardware version if available."""
|
|
if self.data["hw_ver"] is not None:
|
|
return self.data["hw_ver"]
|
|
return None
|
|
|
|
@property
|
|
def mac_address(self) -> Optional[str]:
|
|
"""MAC address if available."""
|
|
if self.data["mac"] is not None:
|
|
return self.data["mac"]
|
|
return None
|
|
|
|
@property
|
|
def manufacturer(self) -> str:
|
|
"""Manufacturer name."""
|
|
return "Dreametech™"
|
|
|
|
@property
|
|
def raw(self) -> dict[str, Any]:
|
|
"""Raw data as returned by the device."""
|
|
return self.data
|