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 cdc2c4db6..b6f01e547 100644 --- a/miio/airhumidifier.py +++ b/miio/airhumidifier.py @@ -254,12 +254,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/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 01fe16ecd..eb33e8bdd 100644 --- a/miio/click_common.py +++ b/miio/click_common.py @@ -168,6 +168,30 @@ 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. %s %s" + % (self._model, self._info) + ) + self._fetch_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 +207,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 +222,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 d3d8622e0..fa9253420 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: List[str] = [] def __init__( self, @@ -63,9 +64,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 @@ -92,6 +97,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( @@ -121,26 +127,59 @@ 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 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): + """Perform miIO.info query on the device and cache the result.""" try: - return DeviceInfo(self.send("miIO.info")) + devinfo = DeviceInfo(self.send("miIO.info")) + self._info = devinfo + _LOGGER.debug("Detected model %s", devinfo.model) + if devinfo.model not in self.supported_models: + _LOGGER.warning( + "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 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 supported_models(self) -> List[str]: + """Return a list of supported models.""" + return self._supported_models + + @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/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 9c3526575..81db61338 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( @@ -418,8 +417,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..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, @@ -145,12 +147,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/dummies.py b/miio/tests/dummies.py index 5d4624eca..74009de2d 100644 --- a/miio/tests/dummies.py +++ b/miio/tests/dummies.py @@ -36,6 +36,12 @@ class DummyDevice: 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" def _reset_state(self): """Revert back to the original state.""" 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 ebd585081..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, @@ -222,7 +223,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 +314,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 77b7b9fda..578c9f93d 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", @@ -470,7 +470,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.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_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_device.py b/miio/tests/test_device.py index cba6afa52..467de5c60 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,40 @@ 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() + + +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 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, diff --git a/miio/tests/test_vacuum.py b/miio/tests/test_vacuum.py index 419acadd1..4398100ea 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, @@ -51,7 +52,6 @@ def __init__(self, *args, **kwargs): } super().__init__(args, kwargs) - self.model = None def change_mode(self, new_mode): if new_mode == "spot": @@ -277,7 +277,16 @@ 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 + + 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 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..9bb50bbe5 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), 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 16b9541e1..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 @@ -20,7 +20,7 @@ LiteralParamType, command, ) -from .device import Device +from .device import Device, DeviceInfo from .exceptions import DeviceException, DeviceInfoUnavailableException from .vacuumcontainers import ( CarpetModeStatus, @@ -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 @@ -119,20 +123,36 @@ 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" + +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, 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,11 +189,37 @@ 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, @@ -537,53 +583,39 @@ 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.""" - 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) + 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: Type[FanspeedEnum] = FanspeedV1 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 - - _LOGGER.debug( - "Using new fanspeed mapping %s for %s", self._fanspeeds, info.model - ) + fanspeeds = FanspeedV2 - @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): diff --git a/miio/yeelight.py b/miio/yeelight.py index bdd523bab..6635c8cfb 100644 --- a/miio/yeelight.py +++ b/miio/yeelight.py @@ -8,6 +8,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 +39,7 @@ class YeelightMode(IntEnum): class YeelightSubLight(DeviceStatus): def __init__(self, data, type): + self.data = data self.type = type @@ -256,6 +259,8 @@ 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."""