From e5bb32aeb7b4a01b5ab1b8e6edb621df1382e624 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 7 May 2021 00:25:35 +0200 Subject: [PATCH 1/8] Add model autodetection Different device models (even when supported by a single integration) can have different features or even different properties/API. Previously, some integrations have used a model argument to define the exact device type to be able to adapt to these differences, but there has been no generalized solution for this issue. This PR will introduce automatic model detection based on the miIO.info query. The first invokation of any command-decorated method will do the query to find the model of the device. The response of this query is cached, so this will only happen once. * info() has now a new keyword-only argument 'skip_cache' which can be used to bypass the cache * Device constructor has a new keyword-only argument to specifying the model (which skips the info query) * This PR converts Vacuum class to use these new facilities for fanspeed controls --- docs/api/miio.rst | 1 + miio/click_common.py | 25 +++++++++ miio/device.py | 115 ++++++++++---------------------------- miio/deviceinfo.py | 89 +++++++++++++++++++++++++++++ miio/tests/dummies.py | 4 ++ miio/tests/test_device.py | 27 +++++++++ miio/tests/test_vacuum.py | 14 ++++- miio/vacuum.py | 106 +++++++++++++++++++++-------------- 8 files changed, 249 insertions(+), 132 deletions(-) create mode 100644 miio/deviceinfo.py diff --git a/docs/api/miio.rst b/docs/api/miio.rst index b628f99fd..d6a438f28 100644 --- a/docs/api/miio.rst +++ b/docs/api/miio.rst @@ -43,6 +43,7 @@ Submodules miio.cooker miio.curtain_youpin miio.device + miio.deviceinfo miio.discovery miio.dreamevacuum_miot miio.exceptions diff --git a/miio/click_common.py b/miio/click_common.py index 01fe16ecd..94fbee9be 100644 --- a/miio/click_common.py +++ b/miio/click_common.py @@ -168,6 +168,27 @@ def __call__(self, func): self.func = func func._device_group_command = self self.kwargs.setdefault("help", self.func.__doc__) + + def _autodetect_model_if_needed(func): + def _wrap(self, *args, **kwargs): + skip_autodetect = func._device_group_command.kwargs.pop( + "skip_autodetect", False + ) + if ( + not skip_autodetect + and self._model is None + and self._info is None + ): + _LOGGER.debug("Unknown model, trying autodetection.") + self.info() + return func(self, *args, **kwargs) + + # TODO HACK to make the command visible to cli + _wrap._device_group_command = func._device_group_command + return _wrap + + func = _autodetect_model_if_needed(func) + return func @property @@ -183,6 +204,9 @@ def wrap(self, ctx, func): else: output = format_output("Running command {0}".format(self.command_name)) + # Remove skip_autodetect before constructing the click.command + self.kwargs.pop("skip_autodetect", None) + func = output(func) for decorator in self.decorators: func = decorator(func) @@ -195,6 +219,7 @@ def call(self, owner, *args, **kwargs): DEFAULT_PARAMS = [ click.Option(["--ip"], required=True, callback=validate_ip), click.Option(["--token"], required=True, callback=validate_token), + click.Option(["--model"], required=False), ] def __init__( diff --git a/miio/device.py b/miio/device.py index 71c7da86e..85d1d17b7 100644 --- a/miio/device.py +++ b/miio/device.py @@ -7,6 +7,7 @@ import click from .click_common import DeviceGroupMeta, LiteralParamType, command, format_output +from .deviceinfo import DeviceInfo from .exceptions import DeviceInfoUnavailableException, PayloadDecodeException from .miioprotocol import MiIOProtocol @@ -20,88 +21,6 @@ class UpdateState(Enum): Idle = "idle" -class DeviceInfo: - """Container of miIO device information. - - Hardware properties such as device model, MAC address, memory information, and - hardware and software information is contained here. - """ - - def __init__(self, data): - """Response of a Xiaomi Smart WiFi Plug. - - {'ap': {'bssid': 'FF:FF:FF:FF:FF:FF', 'rssi': -68, 'ssid': 'network'}, - 'cfg_time': 0, - 'fw_ver': '1.2.4_16', - 'hw_ver': 'MW300', - 'life': 24, - 'mac': '28:FF:FF:FF:FF:FF', - 'mmfree': 30312, - 'model': 'chuangmi.plug.m1', - 'netif': {'gw': '192.168.xxx.x', - 'localIp': '192.168.xxx.x', - 'mask': '255.255.255.0'}, - 'ot': 'otu', - 'ott_stat': [0, 0, 0, 0], - 'otu_stat': [320, 267, 3, 0, 3, 742], - 'token': '2b00042f7481c7b056c4b410d28f33cf', - 'wifi_fw_ver': 'SD878x-14.76.36.p84-702.1.0-WM'} - """ - 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): - """Information about network configuration.""" - return self.data["netif"] - - @property - def accesspoint(self): - """Information about connected wlan accesspoint.""" - 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 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 raw(self): - """Raw data as returned by the device.""" - return self.data - - class DeviceStatus: """Base class for status containers. @@ -144,9 +63,13 @@ def __init__( debug: int = 0, lazy_discover: bool = True, timeout: int = None, + *, + model: str = None, ) -> None: self.ip = ip self.token = token + self._model = model + self._info = None timeout = timeout if timeout is not None else self.timeout self._protocol = MiIOProtocol( ip, token, start_id, debug, lazy_discover, timeout @@ -173,6 +96,7 @@ def send( :param dict parameters: Parameters to send :param int retry_count: How many times to retry on error :param dict extra_parameters: Extra top-level parameters + :param str model: Force model to avoid autodetection """ retry_count = retry_count if retry_count is not None else self.retry_count return self._protocol.send( @@ -202,26 +126,43 @@ def raw_command(self, command, parameters): "Model: {result.model}\n" "Hardware version: {result.hardware_version}\n" "Firmware version: {result.firmware_version}\n", - ) + ), + skip_autodetect=True, ) - def info(self) -> DeviceInfo: - """Get miIO protocol information from the device. + def info(self, *, skip_cache=False) -> DeviceInfo: + """Get (and cache) miIO protocol information from the device. This includes information about connected wlan network, and hardware and software versions. + + :param force bool: Skip the cache """ + if self._info is not None and not skip_cache: + return self._info + try: - return DeviceInfo(self.send("miIO.info")) + devinfo = DeviceInfo(self.send("miIO.info")) + self._info = devinfo + _LOGGER.info("Detected model %s", devinfo.model) + return devinfo except PayloadDecodeException as ex: raise DeviceInfoUnavailableException( "Unable to request miIO.info from the device" ) from ex @property - def raw_id(self): + def raw_id(self) -> int: """Return the last used protocol sequence id.""" return self._protocol.raw_id + @property + def model(self) -> str: + """Return device model.""" + if self._model is not None: + return self._model + + return self.info().model + def update(self, url: str, md5: str): """Start an OTA update.""" payload = { diff --git a/miio/deviceinfo.py b/miio/deviceinfo.py new file mode 100644 index 000000000..39320aeb2 --- /dev/null +++ b/miio/deviceinfo.py @@ -0,0 +1,89 @@ +from typing import Dict, Optional + + +class DeviceInfo: + """Container of miIO device information. + + Hardware properties such as device model, MAC address, memory information, and + hardware and software information is contained here. + """ + + def __init__(self, data): + """Response of a Xiaomi Smart WiFi Plug. + + {'ap': {'bssid': 'FF:FF:FF:FF:FF:FF', 'rssi': -68, 'ssid': 'network'}, + 'cfg_time': 0, + 'fw_ver': '1.2.4_16', + 'hw_ver': 'MW300', + 'life': 24, + 'mac': '28:FF:FF:FF:FF:FF', + 'mmfree': 30312, + 'model': 'chuangmi.plug.m1', + 'netif': {'gw': '192.168.xxx.x', + 'localIp': '192.168.xxx.x', + 'mask': '255.255.255.0'}, + 'ot': 'otu', + 'ott_stat': [0, 0, 0, 0], + 'otu_stat': [320, 267, 3, 0, 3, 742], + 'token': '2b00042f7481c7b056c4b410d28f33cf', + 'wifi_fw_ver': 'SD878x-14.76.36.p84-702.1.0-WM'} + """ + self.data = data + + def __repr__(self): + return "%s v%s (%s) @ %s - token: %s" % ( + self.model, + self.firmware_version, + self.mac_address, + self.ip_address, + self.token, + ) + + @property + def network_interface(self) -> Dict: + """Information about network configuration. + + If unavailable, returns an empty dictionary. + """ + return self.data.get("netif", {}) + + @property + def accesspoint(self): + """Information about connected wlan accesspoint. + + If unavailable, returns an empty dictionary. + """ + return self.data.get("ap", {}) + + @property + def model(self) -> Optional[str]: + """Model string if available.""" + return self.data.get("model") + + @property + def firmware_version(self) -> Optional[str]: + """Firmware version if available.""" + return self.data.get("fw_ver") + + @property + def hardware_version(self) -> Optional[str]: + """Hardware version if available.""" + return self.data.get("hw_ver") + + @property + def mac_address(self) -> Optional[str]: + """MAC address if available.""" + return self.data.get("mac") + + @property + def ip_address(self) -> Optional[str]: + return self.network_interface.get("localIp") + + @property + def token(self) -> Optional[str]: + return self.data.get("token") + + @property + def raw(self): + """Raw data as returned by the device.""" + return self.data diff --git a/miio/tests/dummies.py b/miio/tests/dummies.py index 5d4624eca..73ae55c10 100644 --- a/miio/tests/dummies.py +++ b/miio/tests/dummies.py @@ -36,6 +36,10 @@ class DummyDevice: def __init__(self, *args, **kwargs): self.start_state = self.state.copy() self._protocol = DummyMiIOProtocol(self) + self._model = None + self._info = None + self.token = "ffffffffffffffffffffffffffffffff" + self.ip = "192.0.2.1" def _reset_state(self): """Revert back to the original state.""" diff --git a/miio/tests/test_device.py b/miio/tests/test_device.py index cba6afa52..31bbd631d 100644 --- a/miio/tests/test_device.py +++ b/miio/tests/test_device.py @@ -47,6 +47,7 @@ class CustomDevice(Device): def test_unavailable_device_info_raises(mocker): + """Make sure custom exception is raised if the info payload is invalid.""" send = mocker.patch("miio.Device.send", side_effect=PayloadDecodeException) d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff") @@ -54,3 +55,29 @@ def test_unavailable_device_info_raises(mocker): d.info() assert send.call_count == 1 + + +def test_model_autodetection(mocker): + """Make sure info() gets called if the model is unknown.""" + info = mocker.patch("miio.Device.info") + _ = mocker.patch("miio.Device.send") + + d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff") + + d.raw_command("cmd", {}) + + info.assert_called() + + +def test_forced_model(mocker): + """Make sure info() does not get called automatically if model is given.""" + info = mocker.patch("miio.Device.info") + _ = mocker.patch("miio.Device.send") + + DUMMY_MODEL = "dummy.model" + + d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff", model=DUMMY_MODEL) + d.raw_command("dummy", {}) + + assert d.model == DUMMY_MODEL + info.assert_not_called() diff --git a/miio/tests/test_vacuum.py b/miio/tests/test_vacuum.py index 419acadd1..bd4e68680 100644 --- a/miio/tests/test_vacuum.py +++ b/miio/tests/test_vacuum.py @@ -51,7 +51,7 @@ def __init__(self, *args, **kwargs): } super().__init__(args, kwargs) - self.model = None + def change_mode(self, new_mode): if new_mode == "spot": @@ -277,9 +277,17 @@ def test_history_empty(self): assert len(self.device.clean_history().ids) == 0 + def test_info_no_cloud(self): + """Test the info functionality for non-cloud connected device.""" + from miio.exceptions import DeviceInfoUnavailableException + + assert self.device.carpet_cleaning_mode() is None + with patch("miio.Device.info", side_effect=DeviceInfoUnavailableException()): + assert self.device.info().model == "rockrobo.vacuum.v1" + def test_carpet_cleaning_mode(self): with patch.object(self.device, "send", return_value=[{"carpet_clean_mode": 0}]): - assert self.device.carpet_cleaning_mode() == CarpetCleaningMode.Avoid + assert self.device.carpet_cleaning_mode() == CarpetCleaningMode.Avoid with patch.object(self.device, "send", return_value="unknown_method"): assert self.device.carpet_cleaning_mode() is None @@ -299,4 +307,4 @@ def test_mop_mode(self): assert self.device.mop_mode() == MopMode.Standard with patch.object(self.device, "send", return_value=[32453]): - assert self.device.mop_mode() is None + assert self.device.mop_mode() is None \ No newline at end of file diff --git a/miio/vacuum.py b/miio/vacuum.py index 16b9541e1..2b7597b66 100644 --- a/miio/vacuum.py +++ b/miio/vacuum.py @@ -20,7 +20,7 @@ LiteralParamType, command, ) -from .device import Device +from .device import Device, DeviceInfo from .exceptions import DeviceException, DeviceInfoUnavailableException from .vacuumcontainers import ( CarpetModeStatus, @@ -119,20 +119,26 @@ class CarpetCleaningMode(enum.Enum): ROCKROBO_V1 = "rockrobo.vacuum.v1" ROCKROBO_S5 = "roborock.vacuum.s5" ROCKROBO_S6 = "roborock.vacuum.s6" -ROCKROBO_S6_MAXV = "roborock.vacuum.a10" ROCKROBO_S7 = "roborock.vacuum.a15" +ROCKROBO_S6_MAXV = "roborock.vacuum.a10" +ROCKROBO_E2 = "roborock.vacuum.e2" class Vacuum(Device): """Main class representing the vacuum.""" def __init__( - self, ip: str, token: str = None, start_id: int = 0, debug: int = 0 - ) -> None: - super().__init__(ip, token, start_id, debug) + self, + ip: str, + token: str = None, + start_id: int = 0, + debug: int = 0, + *, + model=None + ): + super().__init__(ip, token, start_id, debug, model=model) self.manual_seqnum = -1 - self.model = None - self._fanspeeds = FanspeedV1 + @command() def start(self): @@ -169,14 +175,41 @@ def resume_or_start(self): return self.start() + @command(skip_autodetect=True) + def info(self, *, force=False): + """Return info about the device. + + This is overrides the base class info to account for gen1 devices that do not + respond to info query properly when not connected to the cloud. + """ + try: + info = super().info(force=force) + return info + except (TypeError, DeviceInfoUnavailableException): + # cloud-blocked vacuums will not return proper payloads + + dummy_v1 = DeviceInfo( + { + "model": ROCKROBO_V1, + "token": self.token, + "netif": {"localIp": self.ip}, + "fw_ver": "1.0_dummy", + } + ) + + self._info = dummy_v1 + _LOGGER.debug( + "Unable to query info, falling back to dummy %s", dummy_v1.model + ) + return self._info + @command() def home(self): """Stop cleaning and return home.""" - if self.model is None: - self._autodetect_model() PAUSE_BEFORE_HOME = [ ROCKROBO_V1, + ] if self.model in PAUSE_BEFORE_HOME: @@ -537,53 +570,42 @@ def fan_speed(self): """Return fan speed.""" return self.send("get_custom_mode")[0] - def _autodetect_model(self): - """Detect the model of the vacuum. + @command() + def fan_speed_presets(self) -> Dict[str, int]: + """Return dictionary containing supported fan speeds.""" + + def _enum_as_dict(speed_enum): + return {x.name: x.value for x in list(speed_enum)} + + if self.model is None: + return _enum_as_dict(FanspeedV1) + + fanspeeds = FanspeedV1 - For the moment this is used only for the fanspeeds, but that could be extended - to cover other supported features. - """ - try: - info = self.info() - self.model = info.model - except (TypeError, DeviceInfoUnavailableException): - # cloud-blocked vacuums will not return proper payloads - self._fanspeeds = FanspeedV1 - self.model = ROCKROBO_V1 - _LOGGER.warning("Unable to query model, falling back to %s", self.model) - return - finally: - _LOGGER.debug("Model: %s", self.model) if self.model == ROCKROBO_V1: _LOGGER.debug("Got robov1, checking for firmware version") - fw_version = info.firmware_version + fw_version = self.info().firmware_version version, build = fw_version.split("_") version = tuple(map(int, version.split("."))) if version >= (3, 5, 8): - self._fanspeeds = FanspeedV3 + fanspeeds = FanspeedV3 elif version == (3, 5, 7): - self._fanspeeds = FanspeedV2 + fanspeeds = FanspeedV2 else: - self._fanspeeds = FanspeedV1 - elif self.model == "roborock.vacuum.e2": - self._fanspeeds = FanspeedE2 + fanspeeds = FanspeedV1 + elif self.model == ROCKROBO_E2: + fanspeeds = FanspeedE2 elif self.model == ROCKROBO_S7: self._fanspeeds = FanspeedS7 else: - self._fanspeeds = FanspeedV2 + fanspeeds = FanspeedV2 - _LOGGER.debug( - "Using new fanspeed mapping %s for %s", self._fanspeeds, info.model - ) - @command() - def fan_speed_presets(self) -> Dict[str, int]: - """Return dictionary containing supported fan speeds.""" - if self.model is None: - self._autodetect_model() + _LOGGER.debug("Using fanspeeds %s for %s", fanspeeds, self.model) + - return {x.name: x.value for x in list(self._fanspeeds)} + return _enum_as_dict(fanspeeds) @command() def sound_info(self): @@ -863,4 +885,4 @@ def cleanup(vac: Vacuum, *args, **kwargs): with open(id_file, "w") as f: json.dump(seqs, f) - return dg + return dg \ No newline at end of file From afd7fde160627ad94ba5036906bddcb8a2d5cefb Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 7 May 2021 00:54:54 +0200 Subject: [PATCH 2/8] tests: self.model -> self._model --- miio/tests/test_airconditioningcompanion.py | 4 ++-- miio/tests/test_airdehumidifier.py | 2 +- miio/tests/test_airfresh.py | 4 ++-- miio/tests/test_airfresh_t2017.py | 4 ++-- miio/tests/test_airhumidifier.py | 6 +++--- miio/tests/test_airhumidifier_jsq.py | 2 +- miio/tests/test_airhumidifier_mjjsq.py | 2 +- miio/tests/test_airpurifier_airdog.py | 6 +++--- miio/tests/test_airqualitymonitor.py | 6 +++--- miio/tests/test_chuangmi_plug.py | 6 +++--- miio/tests/test_fan.py | 8 ++++---- miio/tests/test_fan_leshow.py | 2 +- miio/tests/test_fan_miot.py | 8 ++++---- miio/tests/test_heater.py | 2 +- miio/tests/test_huizuo.py | 8 ++++---- miio/tests/test_philips_bulb.py | 4 ++-- miio/tests/test_philips_rwread.py | 2 +- miio/tests/test_powerstrip.py | 4 ++-- miio/tests/test_toiletlid.py | 2 +- 19 files changed, 41 insertions(+), 41 deletions(-) diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index ebd585081..48b691a6d 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -222,7 +222,7 @@ class DummyAirConditioningCompanionV3(DummyDevice, AirConditioningCompanionV3): def __init__(self, *args, **kwargs): self.state = ["010507950000257301", "011001160100002573", "807"] self.device_prop = {"lumi.0": {"plug_state": ["on"]}} - self.model = MODEL_ACPARTNER_V3 + self._model = MODEL_ACPARTNER_V3 self.last_ir_played = None self.return_values = { @@ -313,7 +313,7 @@ def test_status(self): class DummyAirConditioningCompanionMcn02(DummyDevice, AirConditioningCompanionMcn02): def __init__(self, *args, **kwargs): self.state = ["on", "cool", 28, "small_fan", "on", 441.0] - self.model = MODEL_ACPARTNER_MCN02 + self._model = MODEL_ACPARTNER_MCN02 self.return_values = {"get_prop": self._get_state} self.start_state = self.state.copy() diff --git a/miio/tests/test_airdehumidifier.py b/miio/tests/test_airdehumidifier.py index 5b8de7be0..52c35c6fc 100644 --- a/miio/tests/test_airdehumidifier.py +++ b/miio/tests/test_airdehumidifier.py @@ -17,7 +17,7 @@ class DummyAirDehumidifierV1(DummyDevice, AirDehumidifier): def __init__(self, *args, **kwargs): - self.model = MODEL_DEHUMIDIFIER_V1 + self._model = MODEL_DEHUMIDIFIER_V1 self.dummy_device_info = { "life": 348202, "uid": 1759530000, diff --git a/miio/tests/test_airfresh.py b/miio/tests/test_airfresh.py index 1bf96e292..3a1c705e8 100644 --- a/miio/tests/test_airfresh.py +++ b/miio/tests/test_airfresh.py @@ -17,7 +17,7 @@ class DummyAirFresh(DummyDevice, AirFresh): def __init__(self, *args, **kwargs): - self.model = MODEL_AIRFRESH_VA2 + self._model = MODEL_AIRFRESH_VA2 self.state = { "power": "on", "ptc_state": None, @@ -213,7 +213,7 @@ def filter_life_remaining(): class DummyAirFreshVA4(DummyDevice, AirFresh): def __init__(self, *args, **kwargs): - self.model = MODEL_AIRFRESH_VA4 + self._model = MODEL_AIRFRESH_VA4 self.state = { "power": "on", "ptc_state": "off", diff --git a/miio/tests/test_airfresh_t2017.py b/miio/tests/test_airfresh_t2017.py index 43614c2d9..83dca3b53 100644 --- a/miio/tests/test_airfresh_t2017.py +++ b/miio/tests/test_airfresh_t2017.py @@ -18,7 +18,7 @@ class DummyAirFreshA1(DummyDevice, AirFreshA1): def __init__(self, *args, **kwargs): - self.model = MODEL_AIRFRESH_A1 + self._model = MODEL_AIRFRESH_A1 self.state = { "power": True, "mode": "auto", @@ -185,7 +185,7 @@ def ptc(): class DummyAirFreshT2017(DummyDevice, AirFreshT2017): def __init__(self, *args, **kwargs): - self.model = MODEL_AIRFRESH_T2017 + self._model = MODEL_AIRFRESH_T2017 self.state = { "power": True, "mode": "favourite", diff --git a/miio/tests/test_airhumidifier.py b/miio/tests/test_airhumidifier.py index b391a74af..8d80bb396 100644 --- a/miio/tests/test_airhumidifier.py +++ b/miio/tests/test_airhumidifier.py @@ -19,7 +19,7 @@ class DummyAirHumidifierV1(DummyDevice, AirHumidifier): def __init__(self, *args, **kwargs): - self.model = MODEL_HUMIDIFIER_V1 + self._model = MODEL_HUMIDIFIER_V1 self.dummy_device_info = { "fw_ver": "1.2.9_5033", "token": "68ffffffffffffffffffffffffffffff", @@ -234,7 +234,7 @@ def child_lock(): class DummyAirHumidifierCA1(DummyDevice, AirHumidifier): def __init__(self, *args, **kwargs): - self.model = MODEL_HUMIDIFIER_CA1 + self._model = MODEL_HUMIDIFIER_CA1 self.dummy_device_info = { "fw_ver": "1.6.6", "token": "68ffffffffffffffffffffffffffffff", @@ -466,7 +466,7 @@ def dry(): class DummyAirHumidifierCB1(DummyDevice, AirHumidifier): def __init__(self, *args, **kwargs): - self.model = MODEL_HUMIDIFIER_CB1 + self._model = MODEL_HUMIDIFIER_CB1 self.dummy_device_info = { "fw_ver": "1.2.9_5033", "token": "68ffffffffffffffffffffffffffffff", diff --git a/miio/tests/test_airhumidifier_jsq.py b/miio/tests/test_airhumidifier_jsq.py index b57083c8c..60f3e2536 100644 --- a/miio/tests/test_airhumidifier_jsq.py +++ b/miio/tests/test_airhumidifier_jsq.py @@ -17,7 +17,7 @@ class DummyAirHumidifierJsq(DummyDevice, AirHumidifierJsq): def __init__(self, *args, **kwargs): - self.model = MODEL_HUMIDIFIER_JSQ001 + self._model = MODEL_HUMIDIFIER_JSQ001 self.dummy_device_info = { "life": 575661, diff --git a/miio/tests/test_airhumidifier_mjjsq.py b/miio/tests/test_airhumidifier_mjjsq.py index 871b78235..54f7e3746 100644 --- a/miio/tests/test_airhumidifier_mjjsq.py +++ b/miio/tests/test_airhumidifier_mjjsq.py @@ -15,7 +15,7 @@ class DummyAirHumidifierMjjsq(DummyDevice, AirHumidifierMjjsq): def __init__(self, *args, **kwargs): - self.model = MODEL_HUMIDIFIER_JSQ1 + self._model = MODEL_HUMIDIFIER_JSQ1 self.state = { "Humidifier_Gear": 1, "Humidity_Value": 44, diff --git a/miio/tests/test_airpurifier_airdog.py b/miio/tests/test_airpurifier_airdog.py index adbdabfe5..998a35310 100644 --- a/miio/tests/test_airpurifier_airdog.py +++ b/miio/tests/test_airpurifier_airdog.py @@ -18,7 +18,7 @@ class DummyAirDogX3(DummyDevice, AirDogX3): def __init__(self, *args, **kwargs): - self.model = MODEL_AIRDOG_X3 + self._model = MODEL_AIRDOG_X3 self.state = { "power": "on", "mode": "manual", @@ -149,7 +149,7 @@ def clean_filters(): class DummyAirDogX5(DummyAirDogX3, AirDogX5): def __init__(self, *args, **kwargs): super().__init__(args, kwargs) - self.model = MODEL_AIRDOG_X5 + self._model = MODEL_AIRDOG_X5 self.state = { "power": "on", "mode": "manual", @@ -170,7 +170,7 @@ def airdogx5(request): class DummyAirDogX7SM(DummyAirDogX5, AirDogX7SM): def __init__(self, *args, **kwargs): super().__init__(args, kwargs) - self.model = MODEL_AIRDOG_X7SM + self._model = MODEL_AIRDOG_X7SM self.state["hcho"] = 2 diff --git a/miio/tests/test_airqualitymonitor.py b/miio/tests/test_airqualitymonitor.py index a78f73cad..d391c074b 100644 --- a/miio/tests/test_airqualitymonitor.py +++ b/miio/tests/test_airqualitymonitor.py @@ -15,7 +15,7 @@ class DummyAirQualityMonitorV1(DummyDevice, AirQualityMonitor): def __init__(self, *args, **kwargs): - self.model = MODEL_AIRQUALITYMONITOR_V1 + self._model = MODEL_AIRQUALITYMONITOR_V1 self.state = { "power": "on", "aqi": 34, @@ -85,7 +85,7 @@ def test_status(self): class DummyAirQualityMonitorS1(DummyDevice, AirQualityMonitor): def __init__(self, *args, **kwargs): - self.model = MODEL_AIRQUALITYMONITOR_S1 + self._model = MODEL_AIRQUALITYMONITOR_S1 self.state = { "battery": 100, "co2": 695, @@ -134,7 +134,7 @@ def test_status(self): class DummyAirQualityMonitorB1(DummyDevice, AirQualityMonitor): def __init__(self, *args, **kwargs): - self.model = MODEL_AIRQUALITYMONITOR_B1 + self._model = MODEL_AIRQUALITYMONITOR_B1 self.state = { "co2e": 1466, "humidity": 59.79999923706055, diff --git a/miio/tests/test_chuangmi_plug.py b/miio/tests/test_chuangmi_plug.py index 5962da97b..6c21576ff 100644 --- a/miio/tests/test_chuangmi_plug.py +++ b/miio/tests/test_chuangmi_plug.py @@ -15,7 +15,7 @@ class DummyChuangmiPlugV1(DummyDevice, ChuangmiPlug): def __init__(self, *args, **kwargs): - self.model = MODEL_CHUANGMI_PLUG_V1 + self._model = MODEL_CHUANGMI_PLUG_V1 self.state = {"on": True, "usb_on": True, "temperature": 32} self.return_values = { "get_prop": self._get_state, @@ -86,7 +86,7 @@ def test_usb_off(self): class DummyChuangmiPlugV3(DummyDevice, ChuangmiPlug): def __init__(self, *args, **kwargs): - self.model = MODEL_CHUANGMI_PLUG_V3 + self._model = MODEL_CHUANGMI_PLUG_V3 self.state = {"on": True, "usb_on": True, "temperature": 32, "wifi_led": "off"} self.return_values = { "get_prop": self._get_state, @@ -177,7 +177,7 @@ def wifi_led(): class DummyChuangmiPlugM1(DummyDevice, ChuangmiPlug): def __init__(self, *args, **kwargs): - self.model = MODEL_CHUANGMI_PLUG_M1 + self._model = MODEL_CHUANGMI_PLUG_M1 self.state = {"power": "on", "temperature": 32} self.return_values = { "get_prop": self._get_state, diff --git a/miio/tests/test_fan.py b/miio/tests/test_fan.py index dd29d1562..0ee925b01 100644 --- a/miio/tests/test_fan.py +++ b/miio/tests/test_fan.py @@ -21,7 +21,7 @@ class DummyFanV2(DummyDevice, Fan): def __init__(self, *args, **kwargs): - self.model = MODEL_FAN_V2 + self._model = MODEL_FAN_V2 # This example response is just a guess. Please update! self.state = { "temp_dec": 232, @@ -271,7 +271,7 @@ def delay_off_countdown(): class DummyFanV3(DummyDevice, Fan): def __init__(self, *args, **kwargs): - self.model = MODEL_FAN_V3 + self._model = MODEL_FAN_V3 self.state = { "temp_dec": 232, "humidity": 46, @@ -527,7 +527,7 @@ def delay_off_countdown(): class DummyFanSA1(DummyDevice, Fan): def __init__(self, *args, **kwargs): - self.model = MODEL_FAN_SA1 + self._model = MODEL_FAN_SA1 self.state = { "angle": 120, "speed": 277, @@ -745,7 +745,7 @@ def delay_off_countdown(): class DummyFanP5(DummyDevice, FanP5): def __init__(self, *args, **kwargs): - self.model = MODEL_FAN_P5 + self._model = MODEL_FAN_P5 self.state = { "power": True, "mode": "normal", diff --git a/miio/tests/test_fan_leshow.py b/miio/tests/test_fan_leshow.py index 8abd703f7..2f767137c 100644 --- a/miio/tests/test_fan_leshow.py +++ b/miio/tests/test_fan_leshow.py @@ -15,7 +15,7 @@ class DummyFanLeshow(DummyDevice, FanLeshow): def __init__(self, *args, **kwargs): - self.model = MODEL_FAN_LESHOW_SS4 + self._model = MODEL_FAN_LESHOW_SS4 self.state = { "power": 1, "mode": 2, diff --git a/miio/tests/test_fan_miot.py b/miio/tests/test_fan_miot.py index e80834ea7..682317ec7 100644 --- a/miio/tests/test_fan_miot.py +++ b/miio/tests/test_fan_miot.py @@ -17,7 +17,7 @@ class DummyFanMiot(DummyMiotDevice, FanMiot): def __init__(self, *args, **kwargs): - self.model = MODEL_FAN_P9 + self._model = MODEL_FAN_P9 self.state = { "power": True, "mode": 0, @@ -176,7 +176,7 @@ def delay_off_countdown(): class DummyFanMiotP10(DummyFanMiot, FanMiot): def __init__(self, *args, **kwargs): super().__init__(args, kwargs) - self.model = MODEL_FAN_P10 + self._model = MODEL_FAN_P10 @pytest.fixture(scope="class") @@ -220,7 +220,7 @@ def angle(): class DummyFanMiotP11(DummyFanMiot, FanMiot): def __init__(self, *args, **kwargs): super().__init__(args, kwargs) - self.model = MODEL_FAN_P11 + self._model = MODEL_FAN_P11 @pytest.fixture(scope="class") @@ -235,7 +235,7 @@ class TestFanMiotP11(TestFanMiotP10, TestCase): class DummyFan1C(DummyMiotDevice, Fan1C): def __init__(self, *args, **kwargs): - self.model = MODEL_FAN_1C + self._model = MODEL_FAN_1C self.state = { "power": True, "mode": 0, diff --git a/miio/tests/test_heater.py b/miio/tests/test_heater.py index 8eb72efe0..2bd0d6402 100644 --- a/miio/tests/test_heater.py +++ b/miio/tests/test_heater.py @@ -10,7 +10,7 @@ class DummyHeater(DummyDevice, Heater): def __init__(self, *args, **kwargs): - self.model = MODEL_HEATER_ZA1 + self._model = MODEL_HEATER_ZA1 # This example response is just a guess. Please update! self.state = { "target_temperature": 24, diff --git a/miio/tests/test_huizuo.py b/miio/tests/test_huizuo.py index 74a1f93b3..3c2c20040 100644 --- a/miio/tests/test_huizuo.py +++ b/miio/tests/test_huizuo.py @@ -39,28 +39,28 @@ class DummyHuizuo(DummyMiotDevice, Huizuo): def __init__(self, *args, **kwargs): self.state = _INITIAL_STATE - self.model = MODEL_HUIZUO_PIS123 + self._model = MODEL_HUIZUO_PIS123 super().__init__(*args, **kwargs) class DummyHuizuoFan(DummyMiotDevice, HuizuoLampFan): def __init__(self, *args, **kwargs): self.state = _INITIAL_STATE_FAN - self.model = MODEL_HUIZUO_FANWY + self._model = MODEL_HUIZUO_FANWY super().__init__(*args, **kwargs) class DummyHuizuoFan2(DummyMiotDevice, HuizuoLampFan): def __init__(self, *args, **kwargs): self.state = _INITIAL_STATE_FAN - self.model = MODEL_HUIZUO_FANWY2 + self._model = MODEL_HUIZUO_FANWY2 super().__init__(*args, **kwargs) class DummyHuizuoHeater(DummyMiotDevice, HuizuoLampHeater): def __init__(self, *args, **kwargs): self.state = _INITIAL_STATE_HEATER - self.model = MODEL_HUIZUO_WYHEAT + self._model = MODEL_HUIZUO_WYHEAT super().__init__(*args, **kwargs) diff --git a/miio/tests/test_philips_bulb.py b/miio/tests/test_philips_bulb.py index 38a9b306d..bac6a2e3e 100644 --- a/miio/tests/test_philips_bulb.py +++ b/miio/tests/test_philips_bulb.py @@ -15,7 +15,7 @@ class DummyPhilipsBulb(DummyDevice, PhilipsBulb): def __init__(self, *args, **kwargs): - self.model = MODEL_PHILIPS_LIGHT_BULB + self._model = MODEL_PHILIPS_LIGHT_BULB self.state = {"power": "on", "bright": 100, "cct": 10, "snm": 0, "dv": 0} self.return_values = { "get_prop": self._get_state, @@ -180,7 +180,7 @@ def scene(): class DummyPhilipsWhiteBulb(DummyDevice, PhilipsWhiteBulb): def __init__(self, *args, **kwargs): - self.model = MODEL_PHILIPS_LIGHT_HBULB + self._model = MODEL_PHILIPS_LIGHT_HBULB self.state = {"power": "on", "bri": 100, "dv": 0} self.return_values = { "get_prop": self._get_state, diff --git a/miio/tests/test_philips_rwread.py b/miio/tests/test_philips_rwread.py index 9cc90912a..3358c93d5 100644 --- a/miio/tests/test_philips_rwread.py +++ b/miio/tests/test_philips_rwread.py @@ -15,7 +15,7 @@ class DummyPhilipsRwread(DummyDevice, PhilipsRwread): def __init__(self, *args, **kwargs): - self.model = MODEL_PHILIPS_LIGHT_RWREAD + self._model = MODEL_PHILIPS_LIGHT_RWREAD self.state = { "power": "on", "bright": 53, diff --git a/miio/tests/test_powerstrip.py b/miio/tests/test_powerstrip.py index ebba6e39a..129c680c8 100644 --- a/miio/tests/test_powerstrip.py +++ b/miio/tests/test_powerstrip.py @@ -16,7 +16,7 @@ class DummyPowerStripV1(DummyDevice, PowerStrip): def __init__(self, *args, **kwargs): - self.model = MODEL_POWER_STRIP_V1 + self._model = MODEL_POWER_STRIP_V1 self.state = { "power": "on", "mode": "normal", @@ -108,7 +108,7 @@ def mode(): class DummyPowerStripV2(DummyDevice, PowerStrip): def __init__(self, *args, **kwargs): - self.model = MODEL_POWER_STRIP_V2 + self._model = MODEL_POWER_STRIP_V2 self.state = { "power": "on", "mode": "normal", diff --git a/miio/tests/test_toiletlid.py b/miio/tests/test_toiletlid.py index 70ae4acae..e80cc2d19 100644 --- a/miio/tests/test_toiletlid.py +++ b/miio/tests/test_toiletlid.py @@ -25,7 +25,7 @@ class DummyToiletlidV1(DummyDevice, Toiletlid): def __init__(self, *args, **kwargs): - self.model = MODEL_TOILETLID_V1 + self._model = MODEL_TOILETLID_V1 self.state = { "is_on": False, "work_state": 1, From 5c941ad3b87539e02147443f4d4b8db1cc99cf95 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 8 May 2021 17:15:03 +0200 Subject: [PATCH 3/8] WIP add some missing models, fix infinite loop, experiment with supported_models --- miio/chuangmi_plug.py | 6 ++-- miio/click_common.py | 7 +++-- miio/device.py | 19 ++++++++++-- miio/tests/dummies.py | 32 ++++++++++++++++++++- miio/tests/test_airconditioner_miot.py | 1 + miio/tests/test_airconditioningcompanion.py | 1 + miio/tests/test_airpurifier.py | 1 + miio/tests/test_vacuum.py | 1 + miio/tests/test_wifirepeater.py | 7 +++++ miio/tests/test_yeelight.py | 13 ++++++++- miio/vacuum.py | 11 +++++++ miio/yeelight.py | 9 +++++- 12 files changed, 99 insertions(+), 9 deletions(-) diff --git a/miio/chuangmi_plug.py b/miio/chuangmi_plug.py index 7e09a4583..3303aa18b 100644 --- a/miio/chuangmi_plug.py +++ b/miio/chuangmi_plug.py @@ -101,9 +101,9 @@ def __init__( super().__init__(ip, token, start_id, debug, lazy_discover) if model in AVAILABLE_PROPERTIES: - self.model = model + self._model = model else: - self.model = MODEL_CHUANGMI_PLUG_M1 + self._model = MODEL_CHUANGMI_PLUG_M1 @command( default_output=format_output( @@ -182,6 +182,8 @@ def __init__( start_id: int = 0, debug: int = 0, lazy_discover: bool = True, + *, + model: str = None ) -> None: super().__init__( ip, token, start_id, debug, lazy_discover, model=MODEL_CHUANGMI_PLUG_M1 diff --git a/miio/click_common.py b/miio/click_common.py index 94fbee9be..eb33e8bdd 100644 --- a/miio/click_common.py +++ b/miio/click_common.py @@ -179,8 +179,11 @@ def _wrap(self, *args, **kwargs): and self._model is None and self._info is None ): - _LOGGER.debug("Unknown model, trying autodetection.") - self.info() + _LOGGER.debug( + "Unknown model, trying autodetection. %s %s" + % (self._model, self._info) + ) + self._fetch_info() return func(self, *args, **kwargs) # TODO HACK to make the command visible to cli diff --git a/miio/device.py b/miio/device.py index 85d1d17b7..b6d60ecf8 100644 --- a/miio/device.py +++ b/miio/device.py @@ -2,7 +2,7 @@ import logging from enum import Enum from pprint import pformat as pf -from typing import Any, Optional # noqa: F401 +from typing import Any, List, Optional # noqa: F401 import click @@ -54,6 +54,7 @@ class Device(metaclass=DeviceGroupMeta): retry_count = 3 timeout = 5 + _supported_models = [] def __init__( self, @@ -135,15 +136,24 @@ def info(self, *, skip_cache=False) -> DeviceInfo: This includes information about connected wlan network, and hardware and software versions. - :param force bool: Skip the cache + :param skip_cache bool: Skip the cache """ if self._info is not None and not skip_cache: return self._info + return self._fetch_info() + + def _fetch_info(self): try: devinfo = DeviceInfo(self.send("miIO.info")) self._info = devinfo _LOGGER.info("Detected model %s", devinfo.model) + if devinfo.model not in self.supported_models: + _LOGGER.warning( + "Found an unsupported model %s, if this is working for you, please open an issue at https://github.com/rytilahti/python-miio/", + self.model, + ) + return devinfo except PayloadDecodeException as ex: raise DeviceInfoUnavailableException( @@ -155,6 +165,11 @@ def raw_id(self) -> int: """Return the last used protocol sequence id.""" return self._protocol.raw_id + @property + def supported_models(self) -> List[str]: + """Return a list of supported models.""" + return self._supported_models + @property def model(self) -> str: """Return device model.""" diff --git a/miio/tests/dummies.py b/miio/tests/dummies.py index 73ae55c10..e6dab57f9 100644 --- a/miio/tests/dummies.py +++ b/miio/tests/dummies.py @@ -1,3 +1,6 @@ +from miio.deviceinfo import DeviceInfo + + class DummyMiIOProtocol: """DummyProtocol allows you mock MiIOProtocol.""" @@ -36,7 +39,6 @@ class DummyDevice: def __init__(self, *args, **kwargs): self.start_state = self.state.copy() self._protocol = DummyMiIOProtocol(self) - self._model = None self._info = None self.token = "ffffffffffffffffffffffffffffffff" self.ip = "192.0.2.1" @@ -45,6 +47,34 @@ def _reset_state(self): """Revert back to the original state.""" self.state = self.start_state.copy() + def info(self): + if self._model is None: + self._model = "dummy.model" + + # Dummy model information taken from test_airhumidifer_jsq + dummy_device_info = DeviceInfo( + { + "life": 575661, + "token": "68ffffffffffffffffffffffffffffff", + "mac": "78:11:FF:FF:FF:FF", + "fw_ver": "1.3.9", + "hw_ver": "ESP8266", + "uid": "1111111111", + "model": self._model, + "mcu_fw_ver": "0001", + "wifi_fw_ver": "1.5.0-dev(7efd021)", + "ap": {"rssi": -71, "ssid": "ap", "bssid": "FF:FF:FF:FF:FF:FF"}, + "netif": { + "gw": "192.168.0.1", + "localIp": "192.168.0.25", + "mask": "255.255.255.0", + }, + "mmfree": 228248, + } + ) + + return dummy_device_info + def _set_state(self, var, value): """Set a state of a variable, the value is expected to be an array with length of 1.""" diff --git a/miio/tests/test_airconditioner_miot.py b/miio/tests/test_airconditioner_miot.py index b6e61f18d..02ced996c 100644 --- a/miio/tests/test_airconditioner_miot.py +++ b/miio/tests/test_airconditioner_miot.py @@ -36,6 +36,7 @@ class DummyAirConditionerMiot(DummyMiotDevice, AirConditionerMiot): def __init__(self, *args, **kwargs): + self._model = "xiaomi.aircondition.mc1" self.state = _INITIAL_STATE self.return_values = { "get_prop": self._get_state, diff --git a/miio/tests/test_airconditioningcompanion.py b/miio/tests/test_airconditioningcompanion.py index 48b691a6d..4fe07fab2 100644 --- a/miio/tests/test_airconditioningcompanion.py +++ b/miio/tests/test_airconditioningcompanion.py @@ -67,6 +67,7 @@ class DummyAirConditioningCompanion(DummyDevice, AirConditioningCompanion): def __init__(self, *args, **kwargs): self.state = ["010500978022222102", "01020119A280222221", "2"] self.last_ir_played = None + self._model = "missing.model.airconditioningcompanion" self.return_values = { "get_model_and_state": self._get_state, diff --git a/miio/tests/test_airpurifier.py b/miio/tests/test_airpurifier.py index 8691f7b78..ba2f0189e 100644 --- a/miio/tests/test_airpurifier.py +++ b/miio/tests/test_airpurifier.py @@ -17,6 +17,7 @@ class DummyAirPurifier(DummyDevice, AirPurifier): def __init__(self, *args, **kwargs): + self._model = "missing.model.airpurifier" self.state = { "power": "on", "aqi": 10, diff --git a/miio/tests/test_vacuum.py b/miio/tests/test_vacuum.py index bd4e68680..ee602f580 100644 --- a/miio/tests/test_vacuum.py +++ b/miio/tests/test_vacuum.py @@ -23,6 +23,7 @@ class DummyVacuum(DummyDevice, Vacuum): STATE_MANUAL = 7 def __init__(self, *args, **kwargs): + self._model = "missing.model.vacuum" self.state = { "state": 8, "dnd_enabled": 1, diff --git a/miio/tests/test_wifirepeater.py b/miio/tests/test_wifirepeater.py index 236c39ef8..5fca85636 100644 --- a/miio/tests/test_wifirepeater.py +++ b/miio/tests/test_wifirepeater.py @@ -9,6 +9,7 @@ class DummyWifiRepeater(DummyDevice, WifiRepeater): def __init__(self, *args, **kwargs): + self._model = "xiaomi.repeater.v2" self.state = { "sta": {"count": 2, "access_policy": 0}, "mat": [ @@ -76,6 +77,12 @@ def __init__(self, *args, **kwargs): self.start_device_info = self.device_info.copy() super().__init__(args, kwargs) + def info(self): + """This device has custom miIO.info response.""" + from miio.deviceinfo import DeviceInfo + + return DeviceInfo(self.device_info) + def _reset_state(self): """Revert back to the original state.""" self.state = self.start_state.copy() diff --git a/miio/tests/test_yeelight.py b/miio/tests/test_yeelight.py index b578e11ea..f10a992a3 100644 --- a/miio/tests/test_yeelight.py +++ b/miio/tests/test_yeelight.py @@ -10,6 +10,8 @@ class DummyLight(DummyDevice, Yeelight): def __init__(self, *args, **kwargs): + self._model = "missing.model.yeelight" + self.return_values = { "get_prop": self._get_state, "set_power": lambda x: self._set_state("power", x), @@ -70,12 +72,15 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + @pytest.fixture(scope="class") def dummycommonbulb(request): request.cls.device = DummyCommonBulb() # TODO add ability to test on a real device + + @pytest.mark.usefixtures("dummycommonbulb") class TestYeelightCommon(TestCase): def test_on(self): @@ -304,6 +309,7 @@ def test_set_hsv(self): self.device.set_hsv() + class DummyLightCeilingV1(DummyLight): # without background light def __init__(self, *args, **kwargs): self.state = { @@ -336,12 +342,15 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + @pytest.fixture(scope="class") def dummylightceilingv1(request): request.cls.device = DummyLightCeilingV1() # TODO add ability to test on a real device + + @pytest.mark.usefixtures("dummylightceilingv1") class TestYeelightLightCeilingV1(TestCase): def test_status(self): @@ -350,6 +359,7 @@ def test_status(self): assert repr(status) == repr(YeelightStatus(self.device.start_state)) + assert status.name == self.device.start_state["name"] assert status.developer_mode is True assert status.save_state_on_change is True @@ -388,6 +398,7 @@ def test_status(self): assert status.moonlight_mode_brightness == 100 + class DummyLightCeilingV2(DummyLight): # without background light def __init__(self, *args, **kwargs): self.state = { @@ -480,4 +491,4 @@ def test_status(self): assert status.lights[1].color_flowing is False assert status.lights[1].color_flow_params is None assert status.moonlight_mode is True - assert status.moonlight_mode_brightness == 100 + assert status.moonlight_mode_brightness == 100 \ No newline at end of file diff --git a/miio/vacuum.py b/miio/vacuum.py index 2b7597b66..0b758fe33 100644 --- a/miio/vacuum.py +++ b/miio/vacuum.py @@ -123,10 +123,21 @@ class CarpetCleaningMode(enum.Enum): ROCKROBO_S6_MAXV = "roborock.vacuum.a10" ROCKROBO_E2 = "roborock.vacuum.e2" +SUPPORTED_MODELS = [ + ROCKROBO_V1, + ROCKROBO_S5, + ROCKROBO_S6, + ROCKROBO_S7, + ROCKROBO_S6_MAXV, + ROCKROBO_E2, +] + class Vacuum(Device): """Main class representing the vacuum.""" + _supported_models = SUPPORTED_MODELS + def __init__( self, ip: str, diff --git a/miio/yeelight.py b/miio/yeelight.py index bdd523bab..969017d46 100644 --- a/miio/yeelight.py +++ b/miio/yeelight.py @@ -1,3 +1,4 @@ + from enum import IntEnum from typing import List, Optional, Tuple @@ -8,6 +9,8 @@ from .exceptions import DeviceException from .utils import int_to_rgb, rgb_to_int +SUPPORTED_MODELS = ["yeelink.light.color1"] + class YeelightException(DeviceException): pass @@ -37,6 +40,7 @@ class YeelightMode(IntEnum): class YeelightSubLight(DeviceStatus): def __init__(self, data, type): + self.data = data self.type = type @@ -256,7 +260,10 @@ class Yeelight(Device): which however requires enabling the developer mode on the bulbs. """ + _supported_models = SUPPORTED_MODELS + @command(default_output=format_output("", "{result.cli_format}")) + def status(self) -> YeelightStatus: """Retrieve properties.""" properties = [ @@ -422,4 +429,4 @@ def dump_ble_debug(self, table): def set_scene(self, scene, *vals): """Set the scene.""" raise NotImplementedError("Setting the scene is not implemented yet.") - # return self.send("set_scene", [scene, *vals]) + # return self.send("set_scene", [scene, *vals]) \ No newline at end of file From 56bbb563d4954b89dc2824addd00aca6648d32d0 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 16 Jun 2021 18:45:37 +0200 Subject: [PATCH 4/8] convert all devices to use the parent ctor's model kwarg, fix mypy errors --- miio/airconditioningcompanion.py | 7 ++----- miio/airdehumidifier.py | 7 +------ miio/airfresh.py | 7 +------ miio/airfresh_t2017.py | 14 ++------------ miio/airhumidifier.py | 7 +------ miio/airhumidifier_jsq.py | 6 +++--- miio/airhumidifier_mjjsq.py | 7 +------ miio/airpurifier_airdog.py | 21 +++------------------ miio/airqualitymonitor.py | 15 ++------------- miio/device.py | 2 +- miio/fan.py | 14 ++------------ miio/fan_leshow.py | 7 +------ miio/fan_miot.py | 6 ++---- miio/heater.py | 7 +------ miio/huizuo.py | 8 +++----- miio/miot_device.py | 5 ++++- miio/philips_bulb.py | 14 ++------------ miio/philips_rwread.py | 7 +------ miio/powerstrip.py | 7 +------ miio/pwzn_relay.py | 7 +------ miio/tests/test_vacuum.py | 7 +++---- miio/tests/test_yeelight.py | 11 +---------- miio/toiletlid.py | 7 +------ miio/vacuum.py | 29 ++++++++++++++--------------- miio/yeelight.py | 4 +--- 25 files changed, 55 insertions(+), 178 deletions(-) diff --git a/miio/airconditioningcompanion.py b/miio/airconditioningcompanion.py index 6c522fb8c..aba46c6e7 100644 --- a/miio/airconditioningcompanion.py +++ b/miio/airconditioningcompanion.py @@ -236,12 +236,9 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_ACPARTNER_V2, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) - if model in MODELS_SUPPORTED: - self.model = model - else: - self.model = MODEL_ACPARTNER_V2 + if self.model not in MODELS_SUPPORTED: _LOGGER.error( "Device model %s unsupported. Falling back to %s.", model, self.model ) diff --git a/miio/airdehumidifier.py b/miio/airdehumidifier.py index 7f69cbf26..5fd8ccd04 100644 --- a/miio/airdehumidifier.py +++ b/miio/airdehumidifier.py @@ -167,12 +167,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_DEHUMIDIFIER_V1, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_DEHUMIDIFIER_V1 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) self.device_info: DeviceInfo diff --git a/miio/airfresh.py b/miio/airfresh.py index 356cefa67..e346c8140 100644 --- a/miio/airfresh.py +++ b/miio/airfresh.py @@ -227,12 +227,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_AIRFRESH_VA2, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_AIRFRESH_VA2 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/airfresh_t2017.py b/miio/airfresh_t2017.py index db1bf325f..2843c6a19 100644 --- a/miio/airfresh_t2017.py +++ b/miio/airfresh_t2017.py @@ -233,12 +233,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_AIRFRESH_A1, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_AIRFRESH_A1 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( @@ -380,12 +375,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_AIRFRESH_T2017, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_AIRFRESH_T2017 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/airhumidifier.py b/miio/airhumidifier.py index a10453c50..16c3c86cb 100644 --- a/miio/airhumidifier.py +++ b/miio/airhumidifier.py @@ -236,12 +236,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_HUMIDIFIER_V1, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_HUMIDIFIER_V1 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) # TODO: convert to use generic device info in the future self.device_info: Optional[DeviceInfo] = None diff --git a/miio/airhumidifier_jsq.py b/miio/airhumidifier_jsq.py index 16379e543..d9ade0056 100644 --- a/miio/airhumidifier_jsq.py +++ b/miio/airhumidifier_jsq.py @@ -142,9 +142,9 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_HUMIDIFIER_JSQ001, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - self.model = model if model in AVAILABLE_PROPERTIES else MODEL_HUMIDIFIER_JSQ001 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) + if model not in AVAILABLE_PROPERTIES: + self._model = MODEL_HUMIDIFIER_JSQ001 @command( default_output=format_output( diff --git a/miio/airhumidifier_mjjsq.py b/miio/airhumidifier_mjjsq.py index ca3ef0458..51ef7a8cb 100644 --- a/miio/airhumidifier_mjjsq.py +++ b/miio/airhumidifier_mjjsq.py @@ -131,12 +131,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_HUMIDIFIER_MJJSQ, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_HUMIDIFIER_MJJSQ + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/airpurifier_airdog.py b/miio/airpurifier_airdog.py index 90c8e3268..c8bfaa64c 100644 --- a/miio/airpurifier_airdog.py +++ b/miio/airpurifier_airdog.py @@ -109,12 +109,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_AIRDOG_X3, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_AIRDOG_X3 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( @@ -200,12 +195,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_AIRDOG_X5, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_AIRDOG_X5 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) class AirDogX7SM(AirDogX3): @@ -218,9 +208,4 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_AIRDOG_X7SM, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_AIRDOG_X7SM + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) diff --git a/miio/airqualitymonitor.py b/miio/airqualitymonitor.py index 05eaf5587..0f3b92ff4 100644 --- a/miio/airqualitymonitor.py +++ b/miio/airqualitymonitor.py @@ -160,18 +160,12 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_AIRQUALITYMONITOR_V1, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) - if model in AVAILABLE_PROPERTIES: - self.model = model - elif model is not None: - self.model = MODEL_AIRQUALITYMONITOR_V1 + if model not in AVAILABLE_PROPERTIES: _LOGGER.error( "Device model %s unsupported. Falling back to %s.", model, self.model ) - else: - # Force autodetection. - self.model = None @command( default_output=format_output( @@ -191,11 +185,6 @@ def __init__( ) def status(self) -> AirQualityMonitorStatus: """Return device status.""" - - if self.model is None: - info = self.info() - self.model = info.model - properties = AVAILABLE_PROPERTIES[self.model] if self.model == MODEL_AIRQUALITYMONITOR_B1: diff --git a/miio/device.py b/miio/device.py index b6d60ecf8..be5007d0d 100644 --- a/miio/device.py +++ b/miio/device.py @@ -54,7 +54,7 @@ class Device(metaclass=DeviceGroupMeta): retry_count = 3 timeout = 5 - _supported_models = [] + _supported_models: List[str] = [] def __init__( self, diff --git a/miio/fan.py b/miio/fan.py index 222548680..0d2e2d588 100644 --- a/miio/fan.py +++ b/miio/fan.py @@ -284,12 +284,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_FAN_V3, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_FAN_V3 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( @@ -532,12 +527,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_FAN_P5, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_FAN_P5 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/fan_leshow.py b/miio/fan_leshow.py index fe795ef49..2c03daa26 100644 --- a/miio/fan_leshow.py +++ b/miio/fan_leshow.py @@ -102,12 +102,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_FAN_LESHOW_SS4, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_FAN_LESHOW_SS4 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/fan_miot.py b/miio/fan_miot.py index 87c70ee11..76ea3e407 100644 --- a/miio/fan_miot.py +++ b/miio/fan_miot.py @@ -244,8 +244,7 @@ def __init__( if model not in MIOT_MAPPING: raise FanException("Invalid FanMiot model: %s" % model) - super().__init__(ip, token, start_id, debug, lazy_discover) - self.model = model + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( @@ -408,8 +407,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_FAN_1C, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - self.model = model + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/heater.py b/miio/heater.py index 7ebd0ff27..5fd366b66 100644 --- a/miio/heater.py +++ b/miio/heater.py @@ -136,12 +136,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_HEATER_ZA1, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in SUPPORTED_MODELS: - self.model = model - else: - self.model = MODEL_HEATER_ZA1 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/huizuo.py b/miio/huizuo.py index e4c724ef0..3ad54f746 100644 --- a/miio/huizuo.py +++ b/miio/huizuo.py @@ -231,12 +231,10 @@ def __init__( if model in MODELS_WITH_HEATER: self.mapping.update(_ADDITIONAL_MAPPING_HEATER) - super().__init__(ip, token, start_id, debug, lazy_discover) + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) - if model in MODELS_SUPPORTED: - self.model = model - else: - self.model = MODEL_HUIZUO_PIS123 + if model not in MODELS_SUPPORTED: + self._model = MODEL_HUIZUO_PIS123 _LOGGER.error( "Device model %s unsupported. Falling back to %s.", model, self.model ) diff --git a/miio/miot_device.py b/miio/miot_device.py index 413d9ea83..2c4d134f2 100644 --- a/miio/miot_device.py +++ b/miio/miot_device.py @@ -41,10 +41,13 @@ def __init__( lazy_discover: bool = True, timeout: int = None, *, + model: str = None, mapping: MiotMapping = None, ): """Overloaded to accept keyword-only `mapping` parameter.""" - super().__init__(ip, token, start_id, debug, lazy_discover, timeout) + super().__init__( + ip, token, start_id, debug, lazy_discover, timeout, model=model + ) if mapping is None and not hasattr(self, "mapping"): raise DeviceException( diff --git a/miio/philips_bulb.py b/miio/philips_bulb.py index 643de372b..2b409c789 100644 --- a/miio/philips_bulb.py +++ b/miio/philips_bulb.py @@ -77,12 +77,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_PHILIPS_LIGHT_HBULB, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_PHILIPS_LIGHT_HBULB + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( @@ -148,12 +143,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_PHILIPS_LIGHT_BULB, ) -> None: - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_PHILIPS_LIGHT_BULB - - super().__init__(ip, token, start_id, debug, lazy_discover, self.model) + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( click.argument("level", type=int), diff --git a/miio/philips_rwread.py b/miio/philips_rwread.py index 83acc4512..ff6d4b355 100644 --- a/miio/philips_rwread.py +++ b/miio/philips_rwread.py @@ -92,12 +92,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_PHILIPS_LIGHT_RWREAD, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_PHILIPS_LIGHT_RWREAD + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/powerstrip.py b/miio/powerstrip.py index 052591f31..e2e0ea2ce 100644 --- a/miio/powerstrip.py +++ b/miio/powerstrip.py @@ -145,12 +145,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_POWER_STRIP_V1, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_POWER_STRIP_V1 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/pwzn_relay.py b/miio/pwzn_relay.py index 2e7625e15..c0d3871bb 100644 --- a/miio/pwzn_relay.py +++ b/miio/pwzn_relay.py @@ -108,12 +108,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_PWZN_RELAY_APPLE, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_PWZN_RELAY_APPLE + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command(default_output=format_output("", "on_count: {result.on_count}\n")) def status(self) -> PwznRelayStatus: diff --git a/miio/tests/test_vacuum.py b/miio/tests/test_vacuum.py index ee602f580..abb42d00a 100644 --- a/miio/tests/test_vacuum.py +++ b/miio/tests/test_vacuum.py @@ -53,7 +53,6 @@ def __init__(self, *args, **kwargs): super().__init__(args, kwargs) - def change_mode(self, new_mode): if new_mode == "spot": self.state["state"] = DummyVacuum.STATE_SPOT @@ -285,10 +284,10 @@ def test_info_no_cloud(self): assert self.device.carpet_cleaning_mode() is None with patch("miio.Device.info", side_effect=DeviceInfoUnavailableException()): assert self.device.info().model == "rockrobo.vacuum.v1" - + def test_carpet_cleaning_mode(self): with patch.object(self.device, "send", return_value=[{"carpet_clean_mode": 0}]): - assert self.device.carpet_cleaning_mode() == CarpetCleaningMode.Avoid + assert self.device.carpet_cleaning_mode() == CarpetCleaningMode.Avoid with patch.object(self.device, "send", return_value="unknown_method"): assert self.device.carpet_cleaning_mode() is None @@ -308,4 +307,4 @@ def test_mop_mode(self): assert self.device.mop_mode() == MopMode.Standard with patch.object(self.device, "send", return_value=[32453]): - assert self.device.mop_mode() is None \ No newline at end of file + assert self.device.mop_mode() is None diff --git a/miio/tests/test_yeelight.py b/miio/tests/test_yeelight.py index f10a992a3..9bb50bbe5 100644 --- a/miio/tests/test_yeelight.py +++ b/miio/tests/test_yeelight.py @@ -72,15 +72,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - @pytest.fixture(scope="class") def dummycommonbulb(request): request.cls.device = DummyCommonBulb() # TODO add ability to test on a real device - - @pytest.mark.usefixtures("dummycommonbulb") class TestYeelightCommon(TestCase): def test_on(self): @@ -309,7 +306,6 @@ def test_set_hsv(self): self.device.set_hsv() - class DummyLightCeilingV1(DummyLight): # without background light def __init__(self, *args, **kwargs): self.state = { @@ -342,15 +338,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - @pytest.fixture(scope="class") def dummylightceilingv1(request): request.cls.device = DummyLightCeilingV1() # TODO add ability to test on a real device - - @pytest.mark.usefixtures("dummylightceilingv1") class TestYeelightLightCeilingV1(TestCase): def test_status(self): @@ -359,7 +352,6 @@ def test_status(self): assert repr(status) == repr(YeelightStatus(self.device.start_state)) - assert status.name == self.device.start_state["name"] assert status.developer_mode is True assert status.save_state_on_change is True @@ -398,7 +390,6 @@ def test_status(self): assert status.moonlight_mode_brightness == 100 - class DummyLightCeilingV2(DummyLight): # without background light def __init__(self, *args, **kwargs): self.state = { @@ -491,4 +482,4 @@ def test_status(self): assert status.lights[1].color_flowing is False assert status.lights[1].color_flow_params is None assert status.moonlight_mode is True - assert status.moonlight_mode_brightness == 100 \ No newline at end of file + assert status.moonlight_mode_brightness == 100 diff --git a/miio/toiletlid.py b/miio/toiletlid.py index acb13c7da..3cd99dd1b 100644 --- a/miio/toiletlid.py +++ b/miio/toiletlid.py @@ -80,12 +80,7 @@ def __init__( lazy_discover: bool = True, model: str = MODEL_TOILETLID_V1, ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - - if model in AVAILABLE_PROPERTIES: - self.model = model - else: - self.model = MODEL_TOILETLID_V1 + super().__init__(ip, token, start_id, debug, lazy_discover, model=model) @command( default_output=format_output( diff --git a/miio/vacuum.py b/miio/vacuum.py index 0b758fe33..0cad59b0e 100644 --- a/miio/vacuum.py +++ b/miio/vacuum.py @@ -7,7 +7,7 @@ import os import pathlib import time -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Type, Union import click import pytz @@ -53,14 +53,18 @@ class Consumable(enum.Enum): SensorDirty = "sensor_dirty_time" -class FanspeedV1(enum.Enum): +class FanspeedEnum(enum.Enum): + pass + + +class FanspeedV1(FanspeedEnum): Silent = 38 Standard = 60 Medium = 77 Turbo = 90 -class FanspeedV2(enum.Enum): +class FanspeedV2(FanspeedEnum): Silent = 101 Standard = 102 Medium = 103 @@ -69,14 +73,14 @@ class FanspeedV2(enum.Enum): Auto = 106 -class FanspeedV3(enum.Enum): +class FanspeedV3(FanspeedEnum): Silent = 38 Standard = 60 Medium = 75 Turbo = 100 -class FanspeedE2(enum.Enum): +class FanspeedE2(FanspeedEnum): # Original names from the app: Gentle, Silent, Standard, Strong, Max Gentle = 41 Silent = 50 @@ -85,7 +89,7 @@ class FanspeedE2(enum.Enum): Turbo = 100 -class FanspeedS7(enum.Enum): +class FanspeedS7(FanspeedEnum): Silent = 101 Standard = 102 Medium = 103 @@ -150,7 +154,6 @@ def __init__( super().__init__(ip, token, start_id, debug, model=model) self.manual_seqnum = -1 - @command() def start(self): """Start cleaning.""" @@ -220,7 +223,6 @@ def home(self): PAUSE_BEFORE_HOME = [ ROCKROBO_V1, - ] if self.model in PAUSE_BEFORE_HOME: @@ -585,14 +587,13 @@ def fan_speed(self): def fan_speed_presets(self) -> Dict[str, int]: """Return dictionary containing supported fan speeds.""" - def _enum_as_dict(speed_enum): - return {x.name: x.value for x in list(speed_enum)} + def _enum_as_dict(cls): + return {x.name: x.value for x in list(cls)} if self.model is None: return _enum_as_dict(FanspeedV1) - fanspeeds = FanspeedV1 - + fanspeeds: Type[FanspeedEnum] = FanspeedV1 if self.model == ROCKROBO_V1: _LOGGER.debug("Got robov1, checking for firmware version") @@ -612,10 +613,8 @@ def _enum_as_dict(speed_enum): else: fanspeeds = FanspeedV2 - _LOGGER.debug("Using fanspeeds %s for %s", fanspeeds, self.model) - return _enum_as_dict(fanspeeds) @command() @@ -896,4 +895,4 @@ def cleanup(vac: Vacuum, *args, **kwargs): with open(id_file, "w") as f: json.dump(seqs, f) - return dg \ No newline at end of file + return dg diff --git a/miio/yeelight.py b/miio/yeelight.py index 969017d46..6635c8cfb 100644 --- a/miio/yeelight.py +++ b/miio/yeelight.py @@ -1,4 +1,3 @@ - from enum import IntEnum from typing import List, Optional, Tuple @@ -263,7 +262,6 @@ class Yeelight(Device): _supported_models = SUPPORTED_MODELS @command(default_output=format_output("", "{result.cli_format}")) - def status(self) -> YeelightStatus: """Retrieve properties.""" properties = [ @@ -429,4 +427,4 @@ def dump_ble_debug(self, table): def set_scene(self, scene, *vals): """Set the scene.""" raise NotImplementedError("Setting the scene is not implemented yet.") - # return self.send("set_scene", [scene, *vals]) \ No newline at end of file + # return self.send("set_scene", [scene, *vals]) From 6c11f0ab3fd6b3e145d71861136178547d75364e Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 16 Jun 2021 19:06:40 +0200 Subject: [PATCH 5/8] Fix tests --- miio/tests/dummies.py | 34 +++------------------------------- miio/tests/test_vacuum.py | 3 ++- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/miio/tests/dummies.py b/miio/tests/dummies.py index e6dab57f9..74009de2d 100644 --- a/miio/tests/dummies.py +++ b/miio/tests/dummies.py @@ -1,6 +1,3 @@ -from miio.deviceinfo import DeviceInfo - - class DummyMiIOProtocol: """DummyProtocol allows you mock MiIOProtocol.""" @@ -40,6 +37,9 @@ def __init__(self, *args, **kwargs): self.start_state = self.state.copy() self._protocol = DummyMiIOProtocol(self) self._info = None + # TODO: ugly hack to check for pre-existing _model + if getattr(self, "_model", None) is None: + self._model = "dummy.model" self.token = "ffffffffffffffffffffffffffffffff" self.ip = "192.0.2.1" @@ -47,34 +47,6 @@ def _reset_state(self): """Revert back to the original state.""" self.state = self.start_state.copy() - def info(self): - if self._model is None: - self._model = "dummy.model" - - # Dummy model information taken from test_airhumidifer_jsq - dummy_device_info = DeviceInfo( - { - "life": 575661, - "token": "68ffffffffffffffffffffffffffffff", - "mac": "78:11:FF:FF:FF:FF", - "fw_ver": "1.3.9", - "hw_ver": "ESP8266", - "uid": "1111111111", - "model": self._model, - "mcu_fw_ver": "0001", - "wifi_fw_ver": "1.5.0-dev(7efd021)", - "ap": {"rssi": -71, "ssid": "ap", "bssid": "FF:FF:FF:FF:FF:FF"}, - "netif": { - "gw": "192.168.0.1", - "localIp": "192.168.0.25", - "mask": "255.255.255.0", - }, - "mmfree": 228248, - } - ) - - return dummy_device_info - def _set_state(self, var, value): """Set a state of a variable, the value is expected to be an array with length of 1.""" diff --git a/miio/tests/test_vacuum.py b/miio/tests/test_vacuum.py index abb42d00a..4398100ea 100644 --- a/miio/tests/test_vacuum.py +++ b/miio/tests/test_vacuum.py @@ -281,11 +281,12 @@ def test_info_no_cloud(self): """Test the info functionality for non-cloud connected device.""" from miio.exceptions import DeviceInfoUnavailableException - assert self.device.carpet_cleaning_mode() is None with patch("miio.Device.info", side_effect=DeviceInfoUnavailableException()): assert self.device.info().model == "rockrobo.vacuum.v1" def test_carpet_cleaning_mode(self): + assert self.device.carpet_cleaning_mode() is None + with patch.object(self.device, "send", return_value=[{"carpet_clean_mode": 0}]): assert self.device.carpet_cleaning_mode() == CarpetCleaningMode.Avoid From c7937fb888ca8a19dcc7951a2812800787bbd383 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 16 Jun 2021 19:20:37 +0200 Subject: [PATCH 6/8] Add test for unsupported model logging --- miio/device.py | 6 ++++-- miio/tests/test_device.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/miio/device.py b/miio/device.py index be5007d0d..fa9253420 100644 --- a/miio/device.py +++ b/miio/device.py @@ -144,14 +144,16 @@ def info(self, *, skip_cache=False) -> DeviceInfo: return self._fetch_info() def _fetch_info(self): + """Perform miIO.info query on the device and cache the result.""" try: devinfo = DeviceInfo(self.send("miIO.info")) self._info = devinfo - _LOGGER.info("Detected model %s", devinfo.model) + _LOGGER.debug("Detected model %s", devinfo.model) if devinfo.model not in self.supported_models: _LOGGER.warning( - "Found an unsupported model %s, if this is working for you, please open an issue at https://github.com/rytilahti/python-miio/", + "Found an unsupported model '%s' for class '%s'. If this is working for you, please open an issue at https://github.com/rytilahti/python-miio/", self.model, + self.__class__.__name__, ) return devinfo diff --git a/miio/tests/test_device.py b/miio/tests/test_device.py index 31bbd631d..d29d66154 100644 --- a/miio/tests/test_device.py +++ b/miio/tests/test_device.py @@ -77,7 +77,18 @@ def test_forced_model(mocker): DUMMY_MODEL = "dummy.model" d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff", model=DUMMY_MODEL) - d.raw_command("dummy", {}) + d._fetch_info() assert d.model == DUMMY_MODEL info.assert_not_called() + + +def test_missing_supported(mocker, caplog): + """Make sure warning is logged if the device is unsupported for the class.""" + _ = mocker.patch("miio.Device.send") + + d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff") + d._fetch_info() + + assert "Found an unsupported model" in caplog.text + assert "for class 'Device'" in caplog.text From b2be517db2f3e5d4ef713c72d5996a051dbe7505 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 16 Jun 2021 22:59:36 +0200 Subject: [PATCH 7/8] revert mistakenly changed test call for test_forced_model --- miio/tests/test_device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/tests/test_device.py b/miio/tests/test_device.py index d29d66154..467de5c60 100644 --- a/miio/tests/test_device.py +++ b/miio/tests/test_device.py @@ -77,7 +77,7 @@ def test_forced_model(mocker): DUMMY_MODEL = "dummy.model" d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff", model=DUMMY_MODEL) - d._fetch_info() + d.raw_command("dummy", {}) assert d.model == DUMMY_MODEL info.assert_not_called() From 3f389dd51e2984387a0e451025ce7526d0cc14a6 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 16 Jun 2021 23:00:19 +0200 Subject: [PATCH 8/8] powerstrip: add known models to supported models --- miio/powerstrip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miio/powerstrip.py b/miio/powerstrip.py index e2e0ea2ce..e628ec474 100644 --- a/miio/powerstrip.py +++ b/miio/powerstrip.py @@ -136,6 +136,8 @@ def power_factor(self) -> Optional[float]: class PowerStrip(Device): """Main class representing the smart power strip.""" + _supported_models = [MODEL_POWER_STRIP_V1, MODEL_POWER_STRIP_V2] + def __init__( self, ip: str = None,