From 3abffd893b6badcf03f3f37659a6686b9ba30ce1 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 20 Feb 2022 21:12:50 +0100 Subject: [PATCH 1/9] Add pyupgrade to CI runs (#1329) --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e34560d7..ef619f0e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: run: | python -m pip install --upgrade pip poetry poetry install --extras docs + - name: "Run pyupgrade" + run: | + poetry run pre-commit run pyupgrade --all-files - name: "Code formating (black)" run: | poetry run pre-commit run black --all-files From 31554299a1222a8307dfb597c14c6ab39f67009c Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 1 Mar 2022 15:06:26 +0100 Subject: [PATCH 2/9] Mark Roborock S7 MaxV (roborock.vacuum.a27) as supported (#1337) --- miio/integrations/vacuum/roborock/vacuum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index d5187769e..98088ba30 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -142,6 +142,7 @@ class CarpetCleaningMode(enum.Enum): ROCKROBO_T7 = "roborock.vacuum.a11" # cn s7 ROCKROBO_T7S = "roborock.vacuum.a14" ROCKROBO_T7SPLUS = "roborock.vacuum.a23" +ROCKROBO_S7_MAXV = "roborock.vacuum.a27" ROCKROBO_S7 = "roborock.vacuum.a15" ROCKROBO_S6_MAXV = "roborock.vacuum.a10" ROCKROBO_E2 = "roborock.vacuum.e2" @@ -160,6 +161,7 @@ class CarpetCleaningMode(enum.Enum): ROCKROBO_T7S, ROCKROBO_T7SPLUS, ROCKROBO_S7, + ROCKROBO_S7_MAXV, ROCKROBO_S6_MAXV, ROCKROBO_E2, ROCKROBO_1S, From bddb3e98b6b239633c1808510cd24ab49dd91d14 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 2 Mar 2022 16:28:43 +0100 Subject: [PATCH 3/9] Add Viomi V2 (viomi.vacuum.v6) as supported (#1340) --- miio/integrations/vacuum/viomi/viomivacuum.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/viomi/viomivacuum.py b/miio/integrations/vacuum/viomi/viomivacuum.py index a81506617..28d622eb5 100644 --- a/miio/integrations/vacuum/viomi/viomivacuum.py +++ b/miio/integrations/vacuum/viomi/viomivacuum.py @@ -62,7 +62,12 @@ _LOGGER = logging.getLogger(__name__) -SUPPORTED_MODELS = ["viomi.vacuum.v7", "viomi.vacuum.v8", "viomi.vacuum.v10"] +SUPPORTED_MODELS = [ + "viomi.vacuum.v6", + "viomi.vacuum.v7", + "viomi.vacuum.v8", + "viomi.vacuum.v10", +] ERROR_CODES = { 0: "Sleeping and not charging", From 670ecba71385665bfa22213a3a1b7353c66119cb Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 2 Mar 2022 16:40:20 +0100 Subject: [PATCH 4/9] Add PCAP file parser for protocol analysis (#1331) --- devtools/README.md | 9 +++++ devtools/parse_pcap.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 devtools/parse_pcap.py diff --git a/devtools/README.md b/devtools/README.md index f7e18f59b..6926c2dd8 100644 --- a/devtools/README.md +++ b/devtools/README.md @@ -2,6 +2,15 @@ This directory contains tooling useful for developers +## PCAP parser (parse_pcap.py) + +This tool parses PCAP file and tries to decrypt the traffic using the given tokens. Requires typer, dpkt, and rich. +Token option can be used multiple times. All tokens are tested for decryption until decryption succeeds or there are no tokens left to try. + +``` +python pcap_parser.py --token [--token ] +``` + ## MiOT generator This tool generates some boilerplate code for adding support for MIoT devices diff --git a/devtools/parse_pcap.py b/devtools/parse_pcap.py new file mode 100644 index 000000000..fb5524034 --- /dev/null +++ b/devtools/parse_pcap.py @@ -0,0 +1,84 @@ +"""Parse PCAP files for miio traffic.""" +from collections import Counter, defaultdict +from ipaddress import ip_address + +import dpkt +import typer +from dpkt.ethernet import ETH_TYPE_IP, Ethernet +from rich import print + +from miio import Message + +app = typer.Typer() + + +def read_payloads_from_file(file, tokens: list[str]): + """Read the given pcap file and yield src, dst, and result.""" + pcap = dpkt.pcap.Reader(file) + + stats: defaultdict[str, Counter] = defaultdict(Counter) + for _ts, pkt in pcap: + eth = Ethernet(pkt) + if eth.type != ETH_TYPE_IP: + continue + + ip = eth.ip + + if ip.p != 17: + continue + + transport = ip.udp + + if transport.dport != 54321 and transport.sport != 54321: + continue + + data = transport.data + + src_addr = str(ip_address(ip.src)) + dst_addr = str(ip_address(ip.dst)) + + decrypted = None + for token in tokens: + try: + decrypted = Message.parse(data, token=bytes.fromhex(token)) + + break + except BaseException: + continue + + if decrypted is None: + continue + + stats["stats"]["miio_packets"] += 1 + + if decrypted.data.length == 0: + stats["stats"]["empty_packets"] += 1 + continue + + stats["dst_addr"][dst_addr] += 1 + stats["src_addr"][src_addr] += 1 + + payload = decrypted.data.value + + if "result" in payload: + stats["stats"]["results"] += 1 + if "method" in payload: + method = payload["method"] + stats["commands"][method] += 1 + + yield src_addr, dst_addr, payload + + print(stats) # noqa: T001 + + +@app.command() +def read_file( + file: typer.FileBinaryRead, token: list[str] = typer.Option(...) # noqa: B008 +): + """Read PCAP file and output decrypted miio communication.""" + for src_addr, dst_addr, payload in read_payloads_from_file(file, token): + print(f"{src_addr:<15} -> {dst_addr:<15} {payload}") # noqa: T001 + + +if __name__ == "__main__": + app() From b8b9c1ab6a623de8575c96bc21d6715ac5a6b655 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 3 Mar 2022 18:01:07 +0100 Subject: [PATCH 5/9] Deprecate wifi_led in favor of led (#1342) * Deprecate wifi_led in favor of led Doing this makes all devices with leds to have a common interface * Ignore deprecation warnings in Device.__repr__ --- miio/chuangmi_plug.py | 19 ++++++++++++++++++- miio/device.py | 5 ++++- miio/powerstrip.py | 20 +++++++++++++++++++- miio/tests/test_chuangmi_plug.py | 21 ++++++++++++++------- miio/tests/test_powerstrip.py | 21 ++++++++++++++------- 5 files changed, 69 insertions(+), 17 deletions(-) diff --git a/miio/chuangmi_plug.py b/miio/chuangmi_plug.py index b0fd39d4c..cf71894e4 100644 --- a/miio/chuangmi_plug.py +++ b/miio/chuangmi_plug.py @@ -78,8 +78,14 @@ def load_power(self) -> Optional[float]: return float(self.data["load_power"]) return None - @property + @property # type: ignore + @deprecated("Use led()") def wifi_led(self) -> Optional[bool]: + """True if the wifi led is turned on.""" + return self.led + + @property + def led(self) -> Optional[bool]: """True if the wifi led is turned on.""" if "wifi_led" in self.data and self.data["wifi_led"] is not None: return self.data["wifi_led"] == "on" @@ -142,6 +148,7 @@ def usb_off(self): """Power off.""" return self.send("set_usb_off") + @deprecated("Use set_led instead of set_wifi_led") @command( click.argument("wifi_led", type=bool), default_output=format_output( @@ -152,6 +159,16 @@ def usb_off(self): ) def set_wifi_led(self, wifi_led: bool): """Set the wifi led on/off.""" + self.set_led(wifi_led) + + @command( + click.argument("wifi_led", type=bool), + default_output=format_output( + lambda wifi_led: "Turning on LED" if wifi_led else "Turning off LED" + ), + ) + def set_led(self, wifi_led: bool): + """Set the led on/off.""" if wifi_led: return self.send("set_wifi_led", ["on"]) else: diff --git a/miio/device.py b/miio/device.py index 329d02de7..5c8b46ebc 100644 --- a/miio/device.py +++ b/miio/device.py @@ -1,5 +1,6 @@ import inspect import logging +import warnings from enum import Enum from pprint import pformat as pf from typing import Any, List, Optional # noqa: F401 @@ -35,7 +36,9 @@ def __repr__(self): for prop_tuple in props: name, prop = prop_tuple try: - prop_value = prop.fget(self) + # ignore deprecation warnings + with warnings.catch_warnings(): + prop_value = prop.fget(self) except Exception as ex: prop_value = ex.__class__.__name__ diff --git a/miio/powerstrip.py b/miio/powerstrip.py index 469c8ddfe..159c6d80d 100644 --- a/miio/powerstrip.py +++ b/miio/powerstrip.py @@ -8,6 +8,7 @@ from .click_common import EnumType, command, format_output from .device import Device, DeviceStatus from .exceptions import DeviceException +from .utils import deprecated _LOGGER = logging.getLogger(__name__) @@ -97,8 +98,14 @@ def mode(self) -> Optional[PowerMode]: return PowerMode(self.data["mode"]) return None - @property + @property # type: ignore + @deprecated("Use led instead of wifi_led") def wifi_led(self) -> Optional[bool]: + """True if the wifi led is turned on.""" + return self.led + + @property + def led(self) -> Optional[bool]: """True if the wifi led is turned on.""" if "wifi_led" in self.data and self.data["wifi_led"] is not None: return self.data["wifi_led"] == "on" @@ -182,6 +189,7 @@ def set_power_mode(self, mode: PowerMode): # green, normal return self.send("set_power_mode", [mode.value]) + @deprecated("use set_led instead of set_wifi_led") @command( click.argument("led", type=bool), default_output=format_output( @@ -189,6 +197,16 @@ def set_power_mode(self, mode: PowerMode): ), ) def set_wifi_led(self, led: bool): + """Set the wifi led on/off.""" + self.set_led(led) + + @command( + click.argument("led", type=bool), + default_output=format_output( + lambda led: "Turning on LED" if led else "Turning off LED" + ), + ) + def set_led(self, led: bool): """Set the wifi led on/off.""" if led: return self.send("set_wifi_led", ["on"]) diff --git a/miio/tests/test_chuangmi_plug.py b/miio/tests/test_chuangmi_plug.py index 6c21576ff..4d2cfcf50 100644 --- a/miio/tests/test_chuangmi_plug.py +++ b/miio/tests/test_chuangmi_plug.py @@ -164,15 +164,22 @@ def test_usb_off(self): self.device.usb_off() assert self.device.status().usb_power is False - def test_set_wifi_led(self): - def wifi_led(): - return self.device.status().wifi_led + def test_led(self): + def led(): + return self.device.status().led - self.device.set_wifi_led(True) - assert wifi_led() is True + self.device.set_led(True) + assert led() is True - self.device.set_wifi_led(False) - assert wifi_led() is False + self.device.set_led(False) + assert led() is False + + def test_wifi_led_deprecation(self): + with pytest.deprecated_call(): + self.device.set_wifi_led(True) + + with pytest.deprecated_call(): + self.device.status().wifi_led class DummyChuangmiPlugM1(DummyDevice, ChuangmiPlug): diff --git a/miio/tests/test_powerstrip.py b/miio/tests/test_powerstrip.py index 129c680c8..8d297884f 100644 --- a/miio/tests/test_powerstrip.py +++ b/miio/tests/test_powerstrip.py @@ -199,15 +199,22 @@ def mode(): self.device.set_power_mode(PowerMode.Normal) assert mode() == PowerMode.Normal - def test_set_wifi_led(self): - def wifi_led(): - return self.device.status().wifi_led + def test_set_led(self): + def led(): + return self.device.status().led - self.device.set_wifi_led(True) - assert wifi_led() is True + self.device.set_led(True) + assert led() is True - self.device.set_wifi_led(False) - assert wifi_led() is False + self.device.set_led(False) + assert led() is False + + def test_set_wifi_led_deprecation(self): + with pytest.deprecated_call(): + self.device.set_wifi_led(True) + + with pytest.deprecated_call(): + self.device.status().wifi_led def test_set_power_price(self): def power_price(): From 7cc167f530dbd3eae961e243e5ce365924465225 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 4 Mar 2022 01:46:44 +0100 Subject: [PATCH 6/9] Remove deprecated integration classes (#1343) The following previously deprecated classes exist no more: * AirFreshVA4 - use AirFresh * AirHumidifierCA1, AirHumidifierCB1, AirHumidifierCB2 - use AirHumidifier * AirDogX5, AirDogX7SM - use AirDogX3 * AirPurifierMB4 - use AirPurifierMiot * Plug, PlugV1, PlugV3 - use ChuangmiPlug * FanP9, FanP10, FanP11 - use FanMiot * DreameVacuumMiot - use DreameVacuum * Vacuum - use RoborockVacuum --- miio/__init__.py | 19 +++---- miio/airfresh.py | 20 ------- miio/airhumidifier.py | 46 --------------- miio/airpurifier_airdog.py | 29 ---------- miio/airpurifier_miot.py | 8 --- miio/chuangmi_plug.py | 56 ------------------- miio/discovery.py | 16 +++--- miio/integrations/fan/dmaker/__init__.py | 2 +- miio/integrations/fan/dmaker/fan_miot.py | 16 ------ .../vacuum/dreame/dreamevacuum_miot.py | 7 --- miio/integrations/vacuum/roborock/__init__.py | 2 +- .../vacuum/roborock/tests/test_vacuum.py | 10 +--- miio/integrations/vacuum/roborock/vacuum.py | 13 +---- miio/tests/test_airpurifier_airdog.py | 6 +- miio/tests/test_device.py | 4 +- 15 files changed, 24 insertions(+), 230 deletions(-) diff --git a/miio/__init__.py b/miio/__init__.py index bd5f509da..2ba7a8907 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -21,28 +21,28 @@ ) from miio.airconditioningcompanionMCN import AirConditioningCompanionMcn02 from miio.airdehumidifier import AirDehumidifier -from miio.airfresh import AirFresh, AirFreshVA4 +from miio.airfresh import AirFresh from miio.airfresh_t2017 import AirFreshA1, AirFreshT2017 -from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1 +from miio.airhumidifier import AirHumidifier from miio.airhumidifier_jsq import AirHumidifierJsq from miio.airhumidifier_miot import AirHumidifierMiot from miio.airhumidifier_mjjsq import AirHumidifierMjjsq from miio.airpurifier import AirPurifier -from miio.airpurifier_airdog import AirDogX3, AirDogX5, AirDogX7SM -from miio.airpurifier_miot import AirPurifierMB4, AirPurifierMiot +from miio.airpurifier_airdog import AirDogX3 +from miio.airpurifier_miot import AirPurifierMiot from miio.airqualitymonitor import AirQualityMonitor from miio.airqualitymonitor_miot import AirQualityMonitorCGDN1 from miio.aqaracamera import AqaraCamera from miio.chuangmi_camera import ChuangmiCamera from miio.chuangmi_ir import ChuangmiIr -from miio.chuangmi_plug import ChuangmiPlug, Plug, PlugV1, PlugV3 +from miio.chuangmi_plug import ChuangmiPlug from miio.cooker import Cooker from miio.curtain_youpin import CurtainMiot from miio.gateway import Gateway from miio.heater import Heater from miio.heater_miot import HeaterMiot from miio.huizuo import Huizuo, HuizuoLampFan, HuizuoLampHeater, HuizuoLampScene -from miio.integrations.fan.dmaker import Fan1C, FanMiot, FanP5, FanP9, FanP10, FanP11 +from miio.integrations.fan.dmaker import Fan1C, FanMiot, FanP5 from miio.integrations.fan.leshow import FanLeshow from miio.integrations.fan.zhimi import Fan, FanZA5 from miio.integrations.humidifier.deerma import AirHumidifierJsqs @@ -55,12 +55,9 @@ PhilipsWhiteBulb, ) from miio.integrations.petwaterdispenser import PetWaterDispenser -from miio.integrations.vacuum.dreame.dreamevacuum_miot import ( - DreameVacuum, - DreameVacuumMiot, -) +from miio.integrations.vacuum.dreame.dreamevacuum_miot import DreameVacuum from miio.integrations.vacuum.mijia import G1Vacuum -from miio.integrations.vacuum.roborock import RoborockVacuum, Vacuum, VacuumException +from miio.integrations.vacuum.roborock import RoborockVacuum, VacuumException from miio.integrations.vacuum.roborock.vacuumcontainers import ( CleaningDetails, CleaningSummary, diff --git a/miio/airfresh.py b/miio/airfresh.py index a7e004aed..57777933c 100644 --- a/miio/airfresh.py +++ b/miio/airfresh.py @@ -8,7 +8,6 @@ from .click_common import EnumType, command, format_output from .device import Device, DeviceStatus from .exceptions import DeviceException -from .utils import deprecated _LOGGER = logging.getLogger(__name__) @@ -348,22 +347,3 @@ def set_ptc(self, ptc: bool): return self.send("set_ptc_state", ["on"]) else: return self.send("set_ptc_state", ["off"]) - - -@deprecated( - "This will be removed in the future, use AirFresh(..., model='zhimi.airfresh.va4'" -) -class AirFreshVA4(AirFresh): - """Main class representing the air fresh va4.""" - - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - ) -> None: - super().__init__( - ip, token, start_id, debug, lazy_discover, model=MODEL_AIRFRESH_VA4 - ) diff --git a/miio/airhumidifier.py b/miio/airhumidifier.py index 6574adc01..c02079fde 100644 --- a/miio/airhumidifier.py +++ b/miio/airhumidifier.py @@ -8,7 +8,6 @@ from .click_common import EnumType, command, format_output from .device import Device, DeviceInfo, DeviceStatus from .exceptions import DeviceError, DeviceException -from .utils import deprecated _LOGGER = logging.getLogger(__name__) @@ -405,48 +404,3 @@ def set_dry(self, dry: bool): return self.send("set_dry", ["on"]) else: return self.send("set_dry", ["off"]) - - -@deprecated("Use AirHumidifer(model='zhimi.humidifier.ca1") -class AirHumidifierCA1(AirHumidifier): - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - ) -> None: - super().__init__( - ip, token, start_id, debug, lazy_discover, model=MODEL_HUMIDIFIER_CA1 - ) - - -@deprecated("Use AirHumidifer(model='zhimi.humidifier.cb1") -class AirHumidifierCB1(AirHumidifier): - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - ) -> None: - super().__init__( - ip, token, start_id, debug, lazy_discover, model=MODEL_HUMIDIFIER_CB1 - ) - - -@deprecated("Use AirHumidifier(model='zhimi.humidifier.cb2')") -class AirHumidifierCB2(AirHumidifier): - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - ) -> None: - super().__init__( - ip, token, start_id, debug, lazy_discover, model=MODEL_HUMIDIFIER_CB2 - ) diff --git a/miio/airpurifier_airdog.py b/miio/airpurifier_airdog.py index 73f936d64..8f6b0a6a0 100644 --- a/miio/airpurifier_airdog.py +++ b/miio/airpurifier_airdog.py @@ -8,7 +8,6 @@ from .click_common import EnumType, command, format_output from .device import Device, DeviceStatus from .exceptions import DeviceException -from .utils import deprecated _LOGGER = logging.getLogger(__name__) @@ -177,31 +176,3 @@ def set_child_lock(self, lock: bool): def set_filters_cleaned(self): """Set filters cleaned.""" return self.send("set_clean") - - -class AirDogX5(AirDogX3): - @deprecated("Use AirDogX3(model='airdog.airpurifier.x5')") - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - model: str = MODEL_AIRDOG_X5, - ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover, model=model) - - -class AirDogX7SM(AirDogX3): - @deprecated("Use AirDogX3(model='airdog.airpurifier.x7sm')") - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - model: str = MODEL_AIRDOG_X7SM, - ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover, model=model) diff --git a/miio/airpurifier_miot.py b/miio/airpurifier_miot.py index 53bfe1255..3b69db851 100644 --- a/miio/airpurifier_miot.py +++ b/miio/airpurifier_miot.py @@ -4,8 +4,6 @@ import click -from miio.utils import deprecated - from .airfilter_util import FilterType, FilterTypeUtil from .click_common import EnumType, command, format_output from .exceptions import DeviceException @@ -545,9 +543,3 @@ def set_led_brightness_level(self, level: int): raise AirPurifierMiotException("Invalid brightness level: %s" % level) return self.set_property("led_brightness_level", level) - - -class AirPurifierMB4(AirPurifierMiot): - @deprecated("Use AirPurifierMiot") - def __init__(*args, **kwargs): - super().__init__(*args, **kwargs) diff --git a/miio/chuangmi_plug.py b/miio/chuangmi_plug.py index cf71894e4..90d7b9a12 100644 --- a/miio/chuangmi_plug.py +++ b/miio/chuangmi_plug.py @@ -173,59 +173,3 @@ def set_led(self, wifi_led: bool): return self.send("set_wifi_led", ["on"]) else: return self.send("set_wifi_led", ["off"]) - - -@deprecated( - "This device class is deprecated. Please use the ChuangmiPlug " - "class in future and select a model by parameter 'model'." -) -class Plug(ChuangmiPlug): - def __init__( - self, - ip: str = None, - token: str = None, - 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 - ) - - -@deprecated( - "This device class is deprecated. Please use the ChuangmiPlug " - "class in future and select a model by parameter 'model'." -) -class PlugV1(ChuangmiPlug): - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - ) -> None: - super().__init__( - ip, token, start_id, debug, lazy_discover, model=MODEL_CHUANGMI_PLUG_V1 - ) - - -@deprecated( - "This device class is deprecated. Please use the ChuangmiPlug " - "class in future and select a model by parameter 'model'." -) -class PlugV3(ChuangmiPlug): - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - ) -> None: - super().__init__( - ip, token, start_id, debug, lazy_discover, model=MODEL_CHUANGMI_PLUG_V3 - ) diff --git a/miio/discovery.py b/miio/discovery.py index 2cec7f380..458ae3a27 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -15,8 +15,6 @@ AirConditioningCompanion, AirConditioningCompanionMcn02, AirDogX3, - AirDogX5, - AirDogX7SM, AirFresh, AirFreshT2017, AirHumidifier, @@ -43,8 +41,8 @@ PhilipsRwread, PhilipsWhiteBulb, PowerStrip, + RoborockVacuum, Toiletlid, - Vacuum, ViomiVacuum, WaterPurifier, WaterPurifierYunmi, @@ -89,10 +87,10 @@ DEVICE_MAP: Dict[str, Union[Type[Device], partial]] = { - "rockrobo-vacuum-v1": Vacuum, - "roborock-vacuum-s5": Vacuum, - "roborock-vacuum-m1s": Vacuum, - "roborock-vacuum-a10": Vacuum, + "rockrobo-vacuum-v1": RoborockVacuum, + "roborock-vacuum-s5": RoborockVacuum, + "roborock-vacuum-m1s": RoborockVacuum, + "roborock-vacuum-a10": RoborockVacuum, "chuangmi-plug-m1": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_M1), "chuangmi-plug-m3": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_M3), "chuangmi-plug-v1": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_V1), @@ -109,8 +107,8 @@ "xiaomi.aircondition.mc4": AirConditionerMiot, "xiaomi.aircondition.mc5": AirConditionerMiot, "airdog-airpurifier-x3": AirDogX3, - "airdog-airpurifier-x5": AirDogX5, - "airdog-airpurifier-x7sm": AirDogX7SM, + "airdog-airpurifier-x5": AirDogX3, + "airdog-airpurifier-x7sm": AirDogX3, "zhimi-airpurifier-m1": AirPurifier, # mini model "zhimi-airpurifier-m2": AirPurifier, # mini model 2 "zhimi-airpurifier-ma1": AirPurifier, # ms model diff --git a/miio/integrations/fan/dmaker/__init__.py b/miio/integrations/fan/dmaker/__init__.py index f4abffd15..ff42c331d 100644 --- a/miio/integrations/fan/dmaker/__init__.py +++ b/miio/integrations/fan/dmaker/__init__.py @@ -1,3 +1,3 @@ # flake8: noqa from .fan import FanP5 -from .fan_miot import Fan1C, FanMiot, FanP9, FanP10, FanP11 +from .fan_miot import Fan1C, FanMiot diff --git a/miio/integrations/fan/dmaker/fan_miot.py b/miio/integrations/fan/dmaker/fan_miot.py index a0cc50071..b3f78ec20 100644 --- a/miio/integrations/fan/dmaker/fan_miot.py +++ b/miio/integrations/fan/dmaker/fan_miot.py @@ -6,7 +6,6 @@ from miio import DeviceStatus, MiotDevice from miio.click_common import EnumType, command, format_output from miio.fan_common import FanException, MoveDirection, OperationMode -from miio.utils import deprecated MODEL_FAN_P9 = "dmaker.fan.p9" MODEL_FAN_P10 = "dmaker.fan.p10" @@ -371,21 +370,6 @@ def set_rotate(self, direction: MoveDirection): return self.set_property("set_move", value) -@deprecated("Use FanMiot") -class FanP9(FanMiot): - mapping = MIOT_MAPPING[MODEL_FAN_P9] - - -@deprecated("Use FanMiot") -class FanP10(FanMiot): - mapping = MIOT_MAPPING[MODEL_FAN_P10] - - -@deprecated("Use FanMiot") -class FanP11(FanMiot): - mapping = MIOT_MAPPING[MODEL_FAN_P11] - - class Fan1C(MiotDevice): mapping = MIOT_MAPPING[MODEL_FAN_1C] diff --git a/miio/integrations/vacuum/dreame/dreamevacuum_miot.py b/miio/integrations/vacuum/dreame/dreamevacuum_miot.py index 12b2b650a..97c306961 100644 --- a/miio/integrations/vacuum/dreame/dreamevacuum_miot.py +++ b/miio/integrations/vacuum/dreame/dreamevacuum_miot.py @@ -10,7 +10,6 @@ from miio.exceptions import DeviceException from miio.miot_device import DeviceStatus as DeviceStatusContainer from miio.miot_device import MiotDevice, MiotMapping -from miio.utils import deprecated _LOGGER = logging.getLogger(__name__) @@ -610,9 +609,3 @@ def rotate(self, rotatation: int) -> None: }, ], ) - - -class DreameVacuumMiot(DreameVacuum): - @deprecated("DreameVacuumMiot is deprectaed. Use DreameVacuum instead.") - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) diff --git a/miio/integrations/vacuum/roborock/__init__.py b/miio/integrations/vacuum/roborock/__init__.py index 26d58d8b7..b7000e6f2 100644 --- a/miio/integrations/vacuum/roborock/__init__.py +++ b/miio/integrations/vacuum/roborock/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from .vacuum import RoborockVacuum, Vacuum, VacuumException, VacuumStatus +from .vacuum import RoborockVacuum, VacuumException, VacuumStatus diff --git a/miio/integrations/vacuum/roborock/tests/test_vacuum.py b/miio/integrations/vacuum/roborock/tests/test_vacuum.py index 433062b1a..cc964de18 100644 --- a/miio/integrations/vacuum/roborock/tests/test_vacuum.py +++ b/miio/integrations/vacuum/roborock/tests/test_vacuum.py @@ -4,7 +4,7 @@ import pytest -from miio import RoborockVacuum, Vacuum, VacuumStatus +from miio import RoborockVacuum, VacuumStatus from miio.tests.dummies import DummyDevice from ..vacuum import ( @@ -329,14 +329,6 @@ def test_set_mop_intensity_model_check(self): self.device.set_mop_intensity(MopIntensity.Intense) -def test_deprecated_vacuum(caplog): - with pytest.deprecated_call(): - Vacuum("127.1.1.1", "68ffffffffffffffffffffffffffffff") - - with pytest.deprecated_call(): - from miio.vacuum import ROCKROBO_S6 # noqa: F401 - - class DummyVacuumS7(DummyVacuum): def __init__(self, *args, **kwargs): self._model = ROCKROBO_S7 diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 98088ba30..a28a5755a 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -22,7 +22,6 @@ ) from miio.device import Device, DeviceInfo from miio.exceptions import DeviceException, DeviceInfoUnavailableException -from miio.utils import deprecated from .vacuumcontainers import ( CarpetModeStatus, @@ -932,7 +931,7 @@ def callback(ctx, *args, id_file, **kwargs): @dg.resultcallback() @dg.device_pass - def cleanup(vac: Vacuum, *args, **kwargs): + def cleanup(vac: RoborockVacuum, *args, **kwargs): if vac.ip is None: # dummy Device for discovery, skip teardown return id_file = kwargs["id_file"] @@ -945,13 +944,3 @@ def cleanup(vac: Vacuum, *args, **kwargs): json.dump(seqs, f) return dg - - -class Vacuum(RoborockVacuum): - """Main class for roborock vacuums.""" - - @deprecated( - "This class will become the base class for all vacuum implementations. Use RoborockVacuum to control roborock vacuums." - ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) diff --git a/miio/tests/test_airpurifier_airdog.py b/miio/tests/test_airpurifier_airdog.py index 998a35310..08925c983 100644 --- a/miio/tests/test_airpurifier_airdog.py +++ b/miio/tests/test_airpurifier_airdog.py @@ -2,7 +2,7 @@ import pytest -from miio import AirDogX3, AirDogX5, AirDogX7SM +from miio import AirDogX3 from miio.airpurifier_airdog import ( MODEL_AIRDOG_X3, MODEL_AIRDOG_X5, @@ -146,7 +146,7 @@ def clean_filters(): assert clean_filters() is False -class DummyAirDogX5(DummyAirDogX3, AirDogX5): +class DummyAirDogX5(DummyAirDogX3): def __init__(self, *args, **kwargs): super().__init__(args, kwargs) self._model = MODEL_AIRDOG_X5 @@ -167,7 +167,7 @@ def airdogx5(request): # TODO add ability to test on a real device -class DummyAirDogX7SM(DummyAirDogX5, AirDogX7SM): +class DummyAirDogX7SM(DummyAirDogX5): def __init__(self, *args, **kwargs): super().__init__(args, kwargs) self._model = MODEL_AIRDOG_X7SM diff --git a/miio/tests/test_device.py b/miio/tests/test_device.py index 74520ab09..3b0dd5f91 100644 --- a/miio/tests/test_device.py +++ b/miio/tests/test_device.py @@ -2,7 +2,7 @@ import pytest -from miio import Device, MiotDevice, Vacuum +from miio import Device, MiotDevice, RoborockVacuum from miio.exceptions import DeviceInfoUnavailableException, PayloadDecodeException @@ -84,7 +84,7 @@ def test_forced_model(mocker): @pytest.mark.parametrize( - "cls,hidden", [(Device, True), (MiotDevice, True), (Vacuum, False)] + "cls,hidden", [(Device, True), (MiotDevice, True), (RoborockVacuum, False)] ) def test_missing_supported(mocker, caplog, cls, hidden): """Make sure warning is logged if the device is unsupported for the class.""" From 2286e584b08ba31afefffc050e3d3852d31632e7 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 4 Mar 2022 15:53:59 +0100 Subject: [PATCH 7/9] Make sure miotdevice implementations define supported models (#1345) * Add zhimi.fan.za5 as supported to zhimi_miot * Make sure miotdevice implementations define supported models Add model information to: * CurtainMiot * Huizuo * FanMiot, Fan1C * FanZA5 * RoidmiVacuumMiot * YeelightDualControlModule --- miio/curtain_youpin.py | 1 + miio/heater_miot.py | 1 + miio/huizuo.py | 1 + miio/integrations/fan/dmaker/fan_miot.py | 3 +++ miio/integrations/fan/zhimi/zhimi_miot.py | 1 + miio/integrations/vacuum/roidmi/roidmivacuum_miot.py | 1 + miio/tests/test_device.py | 9 ++++++--- miio/yeelight_dual_switch.py | 1 + 8 files changed, 15 insertions(+), 3 deletions(-) diff --git a/miio/curtain_youpin.py b/miio/curtain_youpin.py index 02322a84a..538f41744 100644 --- a/miio/curtain_youpin.py +++ b/miio/curtain_youpin.py @@ -115,6 +115,7 @@ class CurtainMiot(MiotDevice): """Main class representing the lumi.curtain.hagl05 curtain.""" mapping = _MAPPING + _supported_models = ["lumi.curtain.hagl05"] @command( default_output=format_output( diff --git a/miio/heater_miot.py b/miio/heater_miot.py index eed9c3d78..4554452ea 100644 --- a/miio/heater_miot.py +++ b/miio/heater_miot.py @@ -143,6 +143,7 @@ class HeaterMiot(MiotDevice): (zhimi.heater.za2).""" _mappings = _MAPPINGS + _supported_models = list(_MAPPINGS.keys()) @command( default_output=format_output( diff --git a/miio/huizuo.py b/miio/huizuo.py index 3ad54f746..46c2d7cac 100644 --- a/miio/huizuo.py +++ b/miio/huizuo.py @@ -211,6 +211,7 @@ class Huizuo(MiotDevice): """ mapping = _MAPPING + _supported_models = MODELS_SUPPORTED def __init__( self, diff --git a/miio/integrations/fan/dmaker/fan_miot.py b/miio/integrations/fan/dmaker/fan_miot.py index b3f78ec20..0f80a9680 100644 --- a/miio/integrations/fan/dmaker/fan_miot.py +++ b/miio/integrations/fan/dmaker/fan_miot.py @@ -231,6 +231,8 @@ def child_lock(self) -> bool: class FanMiot(MiotDevice): _mappings = MIOT_MAPPING + # TODO Fan1C should be merged to FanMiot + _supported_models = list(set(MIOT_MAPPING) - {MODEL_FAN_1C}) @command( default_output=format_output( @@ -372,6 +374,7 @@ def set_rotate(self, direction: MoveDirection): class Fan1C(MiotDevice): mapping = MIOT_MAPPING[MODEL_FAN_1C] + _supported_models = [MODEL_FAN_1C] def __init__( self, diff --git a/miio/integrations/fan/zhimi/zhimi_miot.py b/miio/integrations/fan/zhimi/zhimi_miot.py index c85280ef7..fed0ab25f 100644 --- a/miio/integrations/fan/zhimi/zhimi_miot.py +++ b/miio/integrations/fan/zhimi/zhimi_miot.py @@ -170,6 +170,7 @@ def temperature(self) -> Any: class FanZA5(MiotDevice): mapping = MIOT_MAPPING + _supported_models = list(MIOT_MAPPING.keys()) @command( default_output=format_output( diff --git a/miio/integrations/vacuum/roidmi/roidmivacuum_miot.py b/miio/integrations/vacuum/roidmi/roidmivacuum_miot.py index 916d9f580..f27128404 100644 --- a/miio/integrations/vacuum/roidmi/roidmivacuum_miot.py +++ b/miio/integrations/vacuum/roidmi/roidmivacuum_miot.py @@ -536,6 +536,7 @@ class RoidmiVacuumMiot(MiotDevice): """Interface for Vacuum Eve Plus (roidmi.vacuum.v60)""" mapping = _MAPPING + _supported_models = ["roidmi.vacuum.v60"] @command() def status(self) -> RoidmiVacuumStatus: diff --git a/miio/tests/test_device.py b/miio/tests/test_device.py index 3b0dd5f91..10f826d4d 100644 --- a/miio/tests/test_device.py +++ b/miio/tests/test_device.py @@ -5,6 +5,8 @@ from miio import Device, MiotDevice, RoborockVacuum from miio.exceptions import DeviceInfoUnavailableException, PayloadDecodeException +DEVICE_CLASSES = Device.__subclasses__() + MiotDevice.__subclasses__() # type: ignore + @pytest.mark.parametrize("max_properties", [None, 1, 15]) def test_get_properties_splitting(mocker, max_properties): @@ -101,10 +103,11 @@ def test_missing_supported(mocker, caplog, cls, hidden): assert f"for class '{cls.__name__}'" in caplog.text -@pytest.mark.parametrize("cls", Device.__subclasses__()) +@pytest.mark.parametrize("cls", DEVICE_CLASSES) def test_device_ctor_model(cls): """Make sure that every device subclass ctor accepts model kwarg.""" - ignore_classes = ["GatewayDevice", "CustomDevice"] + # TODO Huizuo implements custom model fallback, so it needs to be ignored for now + ignore_classes = ["GatewayDevice", "CustomDevice", "Huizuo"] if cls.__name__ in ignore_classes: return @@ -113,7 +116,7 @@ def test_device_ctor_model(cls): assert dev.model == dummy_model -@pytest.mark.parametrize("cls", Device.__subclasses__()) +@pytest.mark.parametrize("cls", DEVICE_CLASSES) def test_device_supported_models(cls): """Make sure that every device subclass has a non-empty supported models.""" if cls.__name__ == "MiotDevice": # skip miotdevice diff --git a/miio/yeelight_dual_switch.py b/miio/yeelight_dual_switch.py index 6bc611e43..b7a9c26e1 100644 --- a/miio/yeelight_dual_switch.py +++ b/miio/yeelight_dual_switch.py @@ -108,6 +108,7 @@ class YeelightDualControlModule(MiotDevice): which uses MIoT protocol.""" mapping = _MAPPING + _supported_models = ["yeelink.switch.sw1"] @command( default_output=format_output( From e901e2c6321b736ef49539c818c6e01bdda20dce Mon Sep 17 00:00:00 2001 From: saxel Date: Mon, 7 Mar 2022 23:39:37 +0700 Subject: [PATCH 8/9] Fix bug for zhimi.fan.za5 resulting in user ack timeout (#1348) * Fix bug for zhimi.fan.za5 resulting in user ack timeout * Update miio/integrations/fan/zhimi/zhimi_miot.py Co-authored-by: Teemu R. Co-authored-by: Teemu R. --- miio/integrations/fan/zhimi/zhimi_miot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/fan/zhimi/zhimi_miot.py b/miio/integrations/fan/zhimi/zhimi_miot.py index fed0ab25f..88a193835 100644 --- a/miio/integrations/fan/zhimi/zhimi_miot.py +++ b/miio/integrations/fan/zhimi/zhimi_miot.py @@ -169,7 +169,7 @@ def temperature(self) -> Any: class FanZA5(MiotDevice): - mapping = MIOT_MAPPING + _mappings = MIOT_MAPPING _supported_models = list(MIOT_MAPPING.keys()) @command( From 6e5b15323eddab34a70c0776adcca82d2b558f1a Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 7 Mar 2022 23:32:24 +0100 Subject: [PATCH 9/9] Release 0.5.11 (#1351) This release fixes zhimi.fan.za5 support and makes all integrations introspectable for their supported models. For developers, there is now a network trace parser (in devtools/parse_pcap.py) that prints the decrypted the traffic for given tokens. The following previously deprecated classes in favor of model-based discovery, if you were using these classes directly you need to adjust your code: * AirFreshVA4 - use AirFresh * AirHumidifierCA1, AirHumidifierCB1, AirHumidifierCB2 - use AirHumidifier * AirDogX5, AirDogX7SM - use AirDogX3 * AirPurifierMB4 - use AirPurifierMiot * Plug, PlugV1, PlugV3 - use ChuangmiPlug * FanP9, FanP10, FanP11 - use FanMiot * DreameVacuumMiot - use DreameVacuum * Vacuum - use RoborockVacuum [Full Changelog](https://github.com/rytilahti/python-miio/compare/0.5.10...0.5.11) **Breaking changes:** - Remove deprecated integration classes [\#1343](https://github.com/rytilahti/python-miio/pull/1343) (@rytilahti) **Implemented enhancements:** - Add PCAP file parser for protocol analysis [\#1331](https://github.com/rytilahti/python-miio/pull/1331) (@rytilahti) **Fixed bugs:** - Fix bug for zhimi.fan.za5 resulting in user ack timeout [\#1348](https://github.com/rytilahti/python-miio/pull/1348) (@saxel) **Deprecated:** - Deprecate wifi\_led in favor of led [\#1342](https://github.com/rytilahti/python-miio/pull/1342) (@rytilahti) **Merged pull requests:** - Make sure miotdevice implementations define supported models [\#1345](https://github.com/rytilahti/python-miio/pull/1345) (@rytilahti) - Add Viomi V2 \(viomi.vacuum.v6\) as supported [\#1340](https://github.com/rytilahti/python-miio/pull/1340) (@rytilahti) - Mark Roborock S7 MaxV \(roborock.vacuum.a27\) as supported [\#1337](https://github.com/rytilahti/python-miio/pull/1337) (@rytilahti) - Add pyupgrade to CI runs [\#1329](https://github.com/rytilahti/python-miio/pull/1329) (@rytilahti) --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac3c38c6..f2eb297e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Change Log +## [0.5.11](https://github.com/rytilahti/python-miio/tree/0.5.11) (2022-03-07) + +This release fixes zhimi.fan.za5 support and makes all integrations introspectable for their supported models. +For developers, there is now a network trace parser (in devtools/parse_pcap.py) that prints the decrypted the traffic for given tokens. + +The following previously deprecated classes in favor of model-based discovery, if you were using these classes directly you need to adjust your code: +* AirFreshVA4 - use AirFresh +* AirHumidifierCA1, AirHumidifierCB1, AirHumidifierCB2 - use AirHumidifier +* AirDogX5, AirDogX7SM - use AirDogX3 +* AirPurifierMB4 - use AirPurifierMiot +* Plug, PlugV1, PlugV3 - use ChuangmiPlug +* FanP9, FanP10, FanP11 - use FanMiot +* DreameVacuumMiot - use DreameVacuum +* Vacuum - use RoborockVacuum + +[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.5.10...0.5.11) + +**Breaking changes:** + +- Remove deprecated integration classes [\#1343](https://github.com/rytilahti/python-miio/pull/1343) (@rytilahti) + +**Implemented enhancements:** + +- Add PCAP file parser for protocol analysis [\#1331](https://github.com/rytilahti/python-miio/pull/1331) (@rytilahti) + +**Fixed bugs:** + +- Fix bug for zhimi.fan.za5 resulting in user ack timeout [\#1348](https://github.com/rytilahti/python-miio/pull/1348) (@saxel) + +**Deprecated:** + +- Deprecate wifi\_led in favor of led [\#1342](https://github.com/rytilahti/python-miio/pull/1342) (@rytilahti) + +**Merged pull requests:** + +- Make sure miotdevice implementations define supported models [\#1345](https://github.com/rytilahti/python-miio/pull/1345) (@rytilahti) +- Add Viomi V2 \(viomi.vacuum.v6\) as supported [\#1340](https://github.com/rytilahti/python-miio/pull/1340) (@rytilahti) +- Mark Roborock S7 MaxV \(roborock.vacuum.a27\) as supported [\#1337](https://github.com/rytilahti/python-miio/pull/1337) (@rytilahti) +- Add pyupgrade to CI runs [\#1329](https://github.com/rytilahti/python-miio/pull/1329) (@rytilahti) + + ## [0.5.10](https://github.com/rytilahti/python-miio/tree/0.5.10) (2022-02-17) This release adds support for several new devices (see details below, thanks to @PRO-2684, @peleccom, @ymj0424, and @supar), and contains improvements to Roborock S7, yeelight and gateway integrations (thanks to @starkillerOG, @Kirmas, and @shred86). Thanks also to everyone who has reported their working model information, we can use this information to provide better discovery in the future and this release silences the warning for known working models. diff --git a/pyproject.toml b/pyproject.toml index 4f66c277a..5e1f64251 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-miio" -version = "0.5.10" +version = "0.5.11" description = "Python library for interfacing with Xiaomi smart appliances" authors = ["Teemu R "] repository = "https://github.com/rytilahti/python-miio"