From a7540d53085f94fe564adc1674e51f3f4671c938 Mon Sep 17 00:00:00 2001 From: martin9000andersen Date: Sat, 12 Jun 2021 11:26:04 +0200 Subject: [PATCH] Initial support for Roidmi Eve --- README.rst | 1 + docs/api/miio.rst | 1 + miio/__init__.py | 1 + miio/roidmivacuum_miot.py | 590 +++++++++++++++++++++++++++ miio/tests/test_roidmivacuum_miot.py | 133 ++++++ 5 files changed, 726 insertions(+) create mode 100644 miio/roidmivacuum_miot.py create mode 100644 miio/tests/test_roidmivacuum_miot.py diff --git a/README.rst b/README.rst index 169ff721f..fee408a20 100644 --- a/README.rst +++ b/README.rst @@ -97,6 +97,7 @@ Supported devices - Xiaomi Mijia 360 1080p - Xiaomi Mijia STYJ02YM (Viomi) - Xiaomi Mijia 1C STYTJ01ZHM (Dreame) +- Xiaomi Roidmi Eve - Xiaomi Mi Smart WiFi Socket - Xiaomi Chuangmi Plug V1 (1 Socket, 1 USB Port) - Xiaomi Chuangmi Plug V3 (1 Socket, 2 USB Ports) diff --git a/docs/api/miio.rst b/docs/api/miio.rst index b628f99fd..7d7924b37 100644 --- a/docs/api/miio.rst +++ b/docs/api/miio.rst @@ -65,6 +65,7 @@ Submodules miio.powerstrip miio.protocol miio.pwzn_relay + miio.roidmivacuum_miot miio.scishare_coffeemaker miio.toiletlid miio.updater diff --git a/miio/__init__.py b/miio/__init__.py index 1cabe73d9..9903641a9 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -49,6 +49,7 @@ from miio.powerstrip import PowerStrip from miio.protocol import Message, Utils from miio.pwzn_relay import PwznRelay +from miio.roidmivacuum_miot import RoidmiVacuumMiot from miio.scishare_coffeemaker import ScishareCoffee from miio.toiletlid import Toiletlid from miio.vacuum import Vacuum, VacuumException diff --git a/miio/roidmivacuum_miot.py b/miio/roidmivacuum_miot.py new file mode 100644 index 000000000..e6b69df8a --- /dev/null +++ b/miio/roidmivacuum_miot.py @@ -0,0 +1,590 @@ +"""Vacuum Eve Plus (roidmi.vacuum.v60)""" + +# https://github.com/rytilahti/python-miio/issues/543#issuecomment-755767331 + +import json +import logging +import math +from enum import Enum + +import click + +from .click_common import EnumType, command, format_output +from .miot_device import DeviceStatus as DeviceStatusContainer +from .miot_device import MiotDevice, MiotMapping + +_LOGGER = logging.getLogger(__name__) + +_MAPPING: MiotMapping = { + "battery_level": {"siid": 3, "piid": 1}, + "charging_state": {"siid": 3, "piid": 2}, + "device_fault": {"siid": 2, "piid": 2}, + "device_status": {"siid": 2, "piid": 1}, + "filter_life_level": {"siid": 10, "piid": 1}, + "filter_left_time": {"siid": 10, "piid": 2}, + "brush_left_time": {"siid": 11, "piid": 1}, + "brush_life_level": {"siid": 11, "piid": 2}, + "brush_left_time2": {"siid": 12, "piid": 1}, + "brush_life_level2": {"siid": 12, "piid": 2}, + "brush_left_time3": {"siid": 15, "piid": 1}, + "brush_life_level3": {"siid": 15, "piid": 2}, + "sweep_mode": {"siid": 14, "piid": 1}, + "cleaning_mode": {"siid": 2, "piid": 4}, + "sweep_type": {"siid": 2, "piid": 8}, + "path_mode": {"siid": 13, "piid": 8}, + "mop_present": {"siid": 8, "piid": 1}, + "work_station_freq": {"siid": 8, "piid": 2}, # Range: [0, 3, 1] + "timing": {"siid": 8, "piid": 6}, # str (example: {"tz":2,"tzs":7200}) + "clean_area": {"siid": 8, "piid": 7}, # uint32 + # "uid": {"siid": 8, "piid": 8}, # str + "auto_boost": {"siid": 8, "piid": 9}, # bool (auto boost on carpet) + "forbid_mode": {"siid": 8, "piid": 10}, # str + "water_level": {"siid": 8, "piid": 11}, + # "siid8_13": {"siid": 8, "piid": 13}, # no-name: (uint32, unit: seconds) (acc: ['read', 'notify']) + # "siid8_14": {"siid": 8, "piid": 14}, # no-name: (uint32, unit: none) (acc: ['read', 'notify']) + "clean_counts": {"siid": 8, "piid": 18}, + # "siid8_19": {"siid": 8, "piid": 19}, # no-name: (uint32, unit: seconds) (acc: ['read', 'notify']) + "double_clean": {"siid": 8, "piid": 20}, + "edge_sweep": {"siid": 8, "piid": 21}, + "led_switch": {"siid": 8, "piid": 22}, + "lidar_collision": {"siid": 8, "piid": 23}, + "station_key": {"siid": 8, "piid": 24}, + "station_led": {"siid": 8, "piid": 25}, + "current_audio": {"siid": 8, "piid": 26}, # str (example: girl_en) + "progress": {"siid": 8, "piid": 28}, + # "station_type": {"siid": 8, "piid": 29}, # uint32 + # "voice_conf": {"siid": 8, "piid": 30}, + # "switch_status": {"siid": 2, "piid": 10}, + "volume": {"siid": 9, "piid": 1}, + "mute": {"siid": 9, "piid": 2}, +} + + +class ChargingState(Enum): + Unknown = -1 + Charging = 1 + Discharging = 2 + NotChargeable = 4 + + +class CleaningMode(Enum): + Unknown = -1 + Silent = 1 + Basic = 2 + Strong = 3 + FullSpeed = 4 + Sweep = 0 + + +class SweepType(Enum): + Unknown = -1 + Sweep = 0 + Mop = 1 + MopAndSweep = 2 + + +class SwitchStatus(Enum): + Unknown = -1 + Open = 1 + + +class PathMode(Enum): + Unknown = -1 + Normal = 0 + YMopping = 1 + RepeatMopping = 2 + + +class WaterLevel(Enum): + Unknown = -1 + First = 1 + Second = 2 + Three = 3 + Fourth = 4 + Mop = 0 + + +class SweepMode(Enum): + Unknown = -1 + Total = 1 + Area = 2 + Curpoint = 3 + Point = 4 + Smart = 7 + AmartArea = 8 + DepthTotal = 9 + AlongWall = 10 + Idle = 0 + + +class FaultStatus(Enum): + Unknown = -1 + NoFaults = 0 + LowBatteryFindCharger = 1 + LowBatteryAndPoweroff = 2 + WheelRap = 3 + CollisionError = 4 + TileDoTask = 5 + LidarPointError = 6 + FrontWallError = 7 + PsdDirty = 8 + MiddleBrushFatal = 9 + SidBrush = 10 + FanSpeedError = 11 + LidarCover = 12 + GarbageBoxFull = 13 + GarbageBoxOut = 14 + GarbageBoxFullOut = 15 + PhysicalTrapped = 16 + PickUpDoTask = 17 + NoWaterBoxDoTask = 18 + WaterBoxEmpty = 19 + CleanCannotArrive = 20 + StartFormForbid = 21 + Drop = 22 + KitWaterPump = 23 + FindChargerFailed = 24 + LowPowerClean = 25 + + +class DeviceStatus(Enum): + Unknown = -1 + Dormant = 1 + Idle = 2 + Paused = 3 + Sweeping = 4 + GoCharging = 5 + Charging = 6 + Error = 7 + Rfctrl = 8 + Fullcharge = 9 + Shutdown = 10 + FindChargerPause = 11 + + +class RoidmiVacuumStatus(DeviceStatusContainer): + def __init__(self, data): + self.data = data + + @property + def battery_level(self) -> int: + return self.data["battery_level"] + + @property + def filter_left_time(self) -> int: + return self.data["filter_left_time"] + + @property + def filter_life_level(self) -> int: + return self.data["filter_life_level"] + + @property + def brush_left_time(self) -> int: + return self.data["brush_left_time"] + + @property + def brush_life_level(self) -> int: + return self.data["brush_life_level"] + + @property + def brush_left_time2(self) -> int: + return self.data["brush_left_time2"] + + @property + def brush_life_level2(self) -> int: + return self.data["brush_life_level2"] + + @property + def brush_left_time3(self) -> int: + return self.data["brush_left_time3"] + + @property + def brush_life_level3(self) -> int: + return self.data["brush_life_level3"] + + @property + def device_fault(self) -> FaultStatus: + try: + return FaultStatus(self.data["device_fault"]) + except ValueError: + _LOGGER.error("Unknown FaultStatus (%s)", self.data["device_fault"]) + return FaultStatus.Unknown + + @property + def charging_state(self) -> ChargingState: + try: + return ChargingState(self.data["charging_state"]) + except ValueError: + _LOGGER.error("Unknown ChargingStats (%s)", self.data["charging_state"]) + return ChargingState.Unknown + + @property + def sweep_mode(self) -> SweepMode: + try: + return SweepMode(self.data["sweep_mode"]) + except ValueError: + _LOGGER.error("Unknown SweepMode (%s)", self.data["sweep_mode"]) + return SweepMode.Unknown + + @property + def cleaning_mode(self) -> CleaningMode: + try: + return CleaningMode(self.data["cleaning_mode"]) + except ValueError: + _LOGGER.error("Unknown CleaningMode (%s)", self.data["cleaning_mode"]) + return CleaningMode.Unknown + + @property + def sweep_type(self) -> SweepType: + try: + return SweepType(self.data["sweep_type"]) + except ValueError: + _LOGGER.error("Unknown SweepType (%s)", self.data["sweep_type"]) + return SweepType.Unknown + + @property + def path_mode(self) -> PathMode: + try: + return PathMode(self.data["path_mode"]) + except ValueError: + _LOGGER.error("Unknown PathMode (%s)", self.data["path_mode"]) + return PathMode.Unknown + + @property + def mop_present(self) -> bool: + return self.data["mop_present"] + + @property + def work_station_freq(self) -> int: + return self.data["work_station_freq"] + + @property + def timing(self) -> str: + return self.data["timing"] + + @property + def clean_area(self) -> int: + return self.data["clean_area"] + + @property + def uid(self) -> int: + return self.data["uid"] + + @property + def auto_boost(self) -> int: + return self.data["auto_boost"] + + def parseForbidMode(self, val): + def secToClock(val): + hour = math.floor(val / 3600) + minut = math.floor((val - hour * 3600) / 60) + return "{}:{:02}".format(hour, minut) + + asDict = json.loads(val) + active = bool(asDict["time"][2]) + begin = secToClock(asDict["time"][0]) + end = secToClock(asDict["time"][1]) + return json.dumps( + {"enabled": active, "begin": begin, "end": end, "tz": asDict["tz"]} + ) + + @property + def forbid_mode(self) -> int: + # Example data: {"time":[75600,21600,1],"tz":2,"tzs":7200} + return self.parseForbidMode(self.data["forbid_mode"]) + + @property + def water_level(self) -> WaterLevel: + try: + return WaterLevel(self.data["water_level"]) + except ValueError: + _LOGGER.error("Unknown WaterLevel (%s)", self.data["water_level"]) + return WaterLevel.Unknown + + # @property + # def siid8_13(self) -> int: + # return self.data["siid8_13"] + + # @property + # def siid8_14(self) -> int: + # return self.data["siid8_14"] + + @property + def clean_counts(self) -> int: + return self.data["clean_counts"] + + # @property + # def siid8_19(self) -> int: + # return self.data["siid8_19"] + + @property + def double_clean(self) -> bool: + return self.data["double_clean"] + + @property + def edge_sweep(self) -> bool: + return self.data["edge_sweep"] + + @property + def led_switch(self) -> bool: + return self.data["led_switch"] + + @property + def lidar_collision(self) -> bool: + return self.data["lidar_collision"] + + @property + def station_key(self) -> bool: + return self.data["station_key"] + + @property + def station_led(self) -> bool: + return self.data["station_led"] + + @property + def current_audio(self) -> str: + return self.data["current_audio"] + + @property + def progress(self) -> str: + return self.data["progress"] + + @property + def voice_conf(self) -> str: + return self.data["voice_conf"] + + @property + def switch_status(self) -> SwitchStatus: + try: + return SwitchStatus(self.data["switch_status"]) + except TypeError: + _LOGGER.error("Unknown SwitchStatus (%s)", self.data["switch_status"]) + return SwitchStatus.Unknown + + @property + def device_status(self) -> DeviceStatus: + try: + return DeviceStatus(self.data["device_status"]) + except TypeError: + _LOGGER.error("Unknown DeviceStatus (%s)", self.data["device_status"]) + return DeviceStatus.Unknown + + @property + def volume(self) -> int: + return self.data["volume"] + + @property + def mute(self) -> bool: + return self.data["mute"] + + +class RoidmiVacuumMiot(MiotDevice): + """Interface for Vacuum Eve Plus (roidmi.vacuum.v60)""" + + mapping = _MAPPING + + @command( + default_output=format_output( + "\n", + "Battery level: {result.battery_level}\n" + "Brush life level: {result.brush_life_level}\n" + "Brush left time: {result.brush_left_time}\n" + "Charging state: {result.charging_state.name}\n" + "Device fault: {result.device_fault.name}\n" + "Device status: {result.device_status.name}\n" + "Filter left level: {result.filter_left_time}\n" + "Filter life level: {result.filter_life_level}\n" + "Operating mode: {result.sweep_mode.name}\n" + "Right side cleaning brush left time: {result.brush_left_time2}\n" + "Right side cleaning brush life level: {result.brush_life_level2}\n" + "Left side cleaning brush left time: {result.brush_left_time3}\n" + "Left side cleaning brush life level: {result.brush_life_level3}\n" + "Cleaning mode: {result.cleaning_mode.name}\n" + "Sweep type: {result.sweep_type.name}\n" + "Path mode: {result.path_mode.name}\n" + "Sweep mode: {result.sweep_mode.name}\n" + "Mop present: {result.mop_present}\n" + "work_station_freq: {result.work_station_freq}\n" + "timing: {result.timing}\n" + "clean_area: {result.clean_area}\n" + # "uid: {result.uid}\n" + "auto_boost: {result.auto_boost}\n" + "forbid_mode: {result.forbid_mode}\n" + "Water level: {result.water_level.name}\n" + # "Unknown siid8_13 [sec]: {result.siid8_13}\n" + # "Unknown siid8_14 [uint32]: {result.siid8_14}\n" + "clean_counts: {result.clean_counts}\n" + # "Unknown siid8_19 [sec]: {result.siid8_19}\n" + "Double clean: {result.double_clean}\n" + "Edge sweep: {result.edge_sweep}\n" + "Led switch: {result.led_switch}\n" + "Lidar collision: {result.lidar_collision}\n" + "Station key: {result.station_key}\n" + "Station led: {result.station_led}\n" + "Current audio: {result.current_audio}\n" + "Progress: {result.progress}\n" + # "Voice config: {result.voice_conf}\n" + # "Switch status: {result.switch_status.name}\n" + "Volume: {result.volume}\n" "Mute: {result.mute}\n", + ) + ) + def status(self) -> RoidmiVacuumStatus: + """State of the vacuum.""" + + return RoidmiVacuumStatus( + { + prop["did"]: prop["value"] if prop["code"] == 0 else None + # max_properties limmit to 10 to avoid "Checksum error" messages from the device. + for prop in self.get_properties_for_mapping(max_properties=10) + } + ) + + @command() + def start(self) -> None: + """Start cleaning.""" + return self.call_action_by(2, 1) + + @command(click.argument("roomstr", type=str)) + def start_room_sweep_unknown(self, roomstr: str) -> None: + """Start cleaning. + + FIXME: the syntax of voice is unknown + """ + return self.call_action_by(2, 3, roomstr) + + @command( + click.argument("sweep_mode", type=EnumType(SweepMode)), + click.argument("clean_info", type=str), + ) + def start_sweep_unknown(self, sweep_mode: SweepMode, clean_info: str) -> None: + """Start sweep with mode. + + FIXME: the syntax of start sweep with mode is unknown + """ + return self.call_action_by(14, 1, [sweep_mode.value, clean_info]) + + @command() + def stop(self) -> None: + """Stop cleaning.""" + return self.call_action_by(2, 2) + + @command() + def home(self) -> None: + """Return to home.""" + return self.call_action_by(3, 1) + + @command() + def identify(self) -> None: + """Locate the device (i am here).""" + return self.call_action_by(8, 1) + + @command(click.argument("vol", type=int)) + def set_sound_volume(self, vol: int): + """Set sound volume [0-100].""" + return self.set_property("volume", vol) + + @command(click.argument("cleaning_mode", type=EnumType(CleaningMode))) + def set_cleaning_mode(self, cleaning_mode: CleaningMode): + """Set cleaning_mode.""" + return self.set_property("cleaning_mode", cleaning_mode.value) + + @command(click.argument("sweep_type", type=EnumType(SweepType))) + def set_sweep_type(self, sweep_type: SweepType): + """Set sweep_type.""" + return self.set_property("sweep_type", sweep_type.value) + + @command(click.argument("path_mode", type=EnumType(PathMode))) + def set_path_mode(self, path_mode: PathMode): + """Set path_mode.""" + return self.set_property("path_mode", path_mode.value) + + @command(click.argument("work_station_freq", type=int)) + def set_work_station_freq(self, work_station_freq: int): + """Set work_station_freq (2 means Auto dust colect every second time).""" + return self.set_property("work_station_freq", work_station_freq) + + @command(click.argument("timing", type=str)) + def set_timing_unknown(self, timing: str): + """Set time zone. + + FIXME: the syntax of timing is unknown + """ + return self.set_property("timing", timing) + + @command(click.argument("auto_boost", type=bool)) + def set_auto_boost(self, auto_boost: bool): + """Set auto boost on carpet.""" + return self.set_property("auto_boost", auto_boost) + + @command( + click.argument("begin", type=str), + click.argument("end", type=str), + click.argument("active", type=bool, required=False, default=True), + ) + def set_forbid_mode(self, begin: str, end: str, active: bool = True): + """Set do not disturbe. + + E.g. begin="22:00" end="05:00" + """ + + def clockToSec(clock): + hour, minut = clock.split(":") + return int(hour) * 3600 + int(minut) * 60 + + begin_int = clockToSec(begin) + end_int = clockToSec(end) + value_str = json.dumps({"time": [begin_int, end_int, int(active)]}) + return self.set_property("forbid_mode", value_str) + + @command(click.argument("water_level", type=EnumType(WaterLevel))) + def set_water_level(self, water_level: WaterLevel): + """Set water_level.""" + return self.set_property("water_level", water_level.value) + + @command(click.argument("double_clean", type=bool)) + def set_double_clean(self, double_clean: bool): + """Set double clean (True/False).""" + return self.set_property("double_clean", double_clean) + + @command(click.argument("edge_sweep", type=bool)) + def set_edge_sweep(self, edge_sweep: bool): + """Set edge_sweep (True/False).""" + return self.set_property("edge_sweep", edge_sweep) + + @command(click.argument("lidar_collision", type=bool)) + def set_lidar_collision(self, lidar_collision: bool): + """Set lidar collision (True/False)..""" + return self.set_property("lidar_collision", lidar_collision) + + @command() + def start_dust(self) -> None: + """Start base dust collection.""" + return self.call_action_by(8, 6) + + @command(click.argument("voice", type=str)) + def set_voice_unknown(self, voice: str) -> None: + """Set voice. + + FIXME: the syntax of voice is unknown + """ + return self.call_action_by(8, 12, voice) + + @command() + def reset_filter_life(self) -> None: + """Reset filter life.""" + return self.call_action_by(10, 1) + + @command() + def reset_mainbrush_life(self) -> None: + """Reset main brush life.""" + return self.call_action_by(11, 1) + + @command() + def reset_sidebrush_life(self) -> None: + """Reset side brush life.""" + return self.call_action_by(12, 1) + + @command() + def reset_sidebrush_left_life(self) -> None: + """Reset side brush life.""" + return self.call_action_by(15, 1) diff --git a/miio/tests/test_roidmivacuum_miot.py b/miio/tests/test_roidmivacuum_miot.py new file mode 100644 index 000000000..3c587c18b --- /dev/null +++ b/miio/tests/test_roidmivacuum_miot.py @@ -0,0 +1,133 @@ +from unittest import TestCase + +import pytest + +from miio import RoidmiVacuumMiot +from miio.roidmivacuum_miot import ( + ChargingState, + CleaningMode, + DeviceStatus, + FaultStatus, + PathMode, + SweepMode, + SweepType, + WaterLevel, +) + +from .dummies import DummyMiotDevice + +_INITIAL_STATE = { + "auto_boost": 1, + "battery_level": 42, + "brush_life_level": 85, + "brush_life_level2": 57, + "brush_life_level3": 60, + "brush_left_time": 235, + "brush_left_time2": 187, + "brush_left_time3": 1096, + "charging_state": ChargingState.Charging, + "cleaning_mode": CleaningMode.FullSpeed, + "current_audio": "girl_en", + "clean_area": 27, + "clean_counts": 778, + "device_fault": FaultStatus.NoFaults, + "device_status": DeviceStatus.Paused, + "double_clean": 0, + "edge_sweep": 0, + "filter_left_time": 154, + "filter_life_level": 66, + "forbid_mode": '{"time":[75600,21600,1],"tz":2,"tzs":7200}', + "led_switch": 0, + "lidar_collision": 1, + "mop_present": 1, + "mute": 0, + "station_key": 0, + "station_led": 0, + # "station_type": {"siid": 8, "piid": 29}, # uint32 + # "switch_status": {"siid": 2, "piid": 10}, + "sweep_mode": SweepMode.Smart, + "sweep_type": SweepType.MopAndSweep, + "timing": '{"tz":2,"tzs":7200}', + "path_mode": PathMode.Normal, + "progress": 57, + "work_station_freq": 1, + # "uid": "12345678", + # "voice_conf": {"siid": 8, "piid": 30}, + "volume": 4, + "water_level": WaterLevel.Mop, + # "siid8_13": {"siid": 8, "piid": 13}, # no-name: (uint32, unit: seconds) (acc: ['read', 'notify']) + # "siid8_14": {"siid": 8, "piid": 14}, # no-name: (uint32, unit: none) (acc: ['read', 'notify']) + # "siid8_19": {"siid": 8, "piid": 19}, # no-name: (uint32, unit: seconds) (acc: ['read', 'notify']) +} + + +class DummyRoidmiVacuumMiot(DummyMiotDevice, RoidmiVacuumMiot): + def __init__(self, *args, **kwargs): + self.state = _INITIAL_STATE + super().__init__(*args, **kwargs) + + +@pytest.fixture(scope="function") +def dummyroidmivacuum(request): + request.cls.device = DummyRoidmiVacuumMiot() + + +def assertEnum(a, b): + assert a == b + assert repr(a) == repr(b) + + +@pytest.mark.usefixtures("dummyroidmivacuum") +class TestRoidmiVacuum(TestCase): + def test_status(self): + status = self.device.status() + assert status.auto_boost == _INITIAL_STATE["auto_boost"] + assert status.battery_level == _INITIAL_STATE["battery_level"] + assert status.brush_left_time == _INITIAL_STATE["brush_left_time"] + assert status.brush_left_time2 == _INITIAL_STATE["brush_left_time2"] + assert status.brush_left_time3 == _INITIAL_STATE["brush_left_time3"] + assert status.brush_life_level == _INITIAL_STATE["brush_life_level"] + assert status.brush_life_level2 == _INITIAL_STATE["brush_life_level2"] + assert status.brush_life_level3 == _INITIAL_STATE["brush_life_level3"] + assertEnum( + status.charging_state, ChargingState(_INITIAL_STATE["charging_state"]) + ) + assertEnum(status.cleaning_mode, CleaningMode(_INITIAL_STATE["cleaning_mode"])) + assert status.current_audio == _INITIAL_STATE["current_audio"] + assert status.clean_area == _INITIAL_STATE["clean_area"] + assert status.clean_counts == _INITIAL_STATE["clean_counts"] + assertEnum(status.device_fault, FaultStatus(_INITIAL_STATE["device_fault"])) + assertEnum(status.device_status, DeviceStatus(_INITIAL_STATE["device_status"])) + assert status.double_clean == _INITIAL_STATE["double_clean"] + assert status.edge_sweep == _INITIAL_STATE["edge_sweep"] + assert status.filter_left_time == _INITIAL_STATE["filter_left_time"] + assert status.filter_life_level == _INITIAL_STATE["filter_life_level"] + assert status.forbid_mode == status.parseForbidMode( + _INITIAL_STATE["forbid_mode"] + ) + assert status.led_switch == _INITIAL_STATE["led_switch"] + assert status.lidar_collision == _INITIAL_STATE["lidar_collision"] + assert status.mop_present == _INITIAL_STATE["mop_present"] + assert status.mute == _INITIAL_STATE["mute"] + assert status.station_key == _INITIAL_STATE["station_key"] + assert status.station_led == _INITIAL_STATE["station_led"] + assertEnum(status.sweep_mode, SweepMode(_INITIAL_STATE["sweep_mode"])) + assertEnum(status.sweep_type, SweepType(_INITIAL_STATE["sweep_type"])) + assert status.timing == _INITIAL_STATE["timing"] + assertEnum(status.path_mode, PathMode(_INITIAL_STATE["path_mode"])) + assert status.progress == _INITIAL_STATE["progress"] + assert status.work_station_freq == _INITIAL_STATE["work_station_freq"] + assert status.volume == _INITIAL_STATE["volume"] + assertEnum(status.water_level, WaterLevel(_INITIAL_STATE["water_level"])) + + def test_parseForbidMode(self): + status = self.device.status() + value = '{"time":[75600,21600,1],"tz":2,"tzs":7200}' + expected_value = '{"enabled": true, "begin": "21:00", "end": "6:00", "tz": 2}' + assert status.parseForbidMode(value) == expected_value + + def test_parseForbidMode2(self): + status = self.device.status() + value = '{"time":[82080,33300,0],"tz":3,"tzs":10800}' + expected_value = '{"enabled": false, "begin": "22:48", "end": "9:15", "tz": 3}' + assert status.parseForbidMode(value) == expected_value