Ver código fonte

Add support for vacuum (#326)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Retha Runolfsson 1 dia atrás
pai
commit
160f715857

+ 2 - 0
switchbot/__init__.py

@@ -32,6 +32,7 @@ from .devices.lock import SwitchbotLock
 from .devices.plug import SwitchbotPlugMini
 from .devices.relay_switch import SwitchbotRelaySwitch
 from .devices.roller_shade import SwitchbotRollerShade
+from .devices.vacuum import SwitchbotVacuum
 from .discovery import GetSwitchbotDevices
 from .models import SwitchBotAdvertisement
 
@@ -66,6 +67,7 @@ __all__ = [
     "SwitchbotRollerShade",
     "SwitchbotSupportedType",
     "SwitchbotSupportedType",
+    "SwitchbotVacuum",
     "close_stale_connections",
     "close_stale_connections_by_address",
     "get_device",

+ 31 - 0
switchbot/adv_parser.py

@@ -33,6 +33,7 @@ from .adv_parsers.relay_switch import (
 )
 from .adv_parsers.remote import process_woremote
 from .adv_parsers.roller_shade import process_worollershade
+from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
 from .const import SwitchbotModel
 from .models import SwitchBotAdvertisement
 
@@ -237,6 +238,36 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
         "func": process_fan,
         "manufacturer_id": 2409,
     },
+    ".": {
+        "modelName": SwitchbotModel.K20_VACUUM,
+        "modelFriendlyName": "K20 Vacuum",
+        "func": process_vacuum,
+        "manufacturer_id": 2409,
+    },
+    "z": {
+        "modelName": SwitchbotModel.S10_VACUUM,
+        "modelFriendlyName": "S10 Vacuum",
+        "func": process_vacuum,
+        "manufacturer_id": 2409,
+    },
+    "3": {
+        "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
+        "modelFriendlyName": "K10+ Pro Combo Vacuum",
+        "func": process_vacuum,
+        "manufacturer_id": 2409,
+    },
+    "}": {
+        "modelName": SwitchbotModel.K10_VACUUM,
+        "modelFriendlyName": "K10+ Vacuum",
+        "func": process_vacuum_k,
+        "manufacturer_id": 2409,
+    },
+    "(": {
+        "modelName": SwitchbotModel.K10_PRO_VACUUM,
+        "modelFriendlyName": "K10+ Pro Vacuum",
+        "func": process_vacuum_k,
+        "manufacturer_id": 2409,
+    },
 }
 
 _SWITCHBOT_MODEL_TO_CHAR = {

+ 61 - 0
switchbot/adv_parsers/vacuum.py

@@ -0,0 +1,61 @@
+"""Vacuum parser."""
+
+from __future__ import annotations
+
+import struct
+
+
+def process_vacuum(
+    data: bytes | None, mfr_data: bytes | None
+) -> dict[str, bool | int | str]:
+    """Support for s10, k10+ pro combo, k20 process service data."""
+    if mfr_data is None:
+        return {}
+
+    _seq_num = mfr_data[6]
+    _soc_version = get_device_fw_version(mfr_data[8:11])
+    # Steps at the end of the last network configuration
+    _step = mfr_data[11] & 0b00001111
+    _mqtt_connected = bool(mfr_data[11] & 0b00010000)
+    _battery = mfr_data[12]
+    _work_status = mfr_data[13] & 0b00111111
+
+    return {
+        "sequence_number": _seq_num,
+        "soc_version": _soc_version,
+        "step": _step,
+        "mqtt_connected": _mqtt_connected,
+        "battery": _battery,
+        "work_status": _work_status,
+    }
+
+
+def get_device_fw_version(version_bytes: bytes) -> str | None:
+    version1 = version_bytes[0] & 0x0F
+    version2 = version_bytes[0] >> 4
+    version3 = struct.unpack("<H", version_bytes[1:])[0]
+    return f"{version1}.{version2}.{version3:>03d}"
+
+
+def process_vacuum_k(
+    data: bytes | None, mfr_data: bytes | None
+) -> dict[str, bool | int | str]:
+    """Support for k10+, k10+ pro process service data."""
+    if mfr_data is None:
+        return {}
+
+    _seq_num = mfr_data[6]
+    _dustbin_bound = bool(mfr_data[7] & 0b10000000)
+    _dusbin_connected = bool(mfr_data[7] & 0b01000000)
+    _network_connected = bool(mfr_data[7] & 0b00100000)
+    _work_status = (mfr_data[7] & 0b00010000) >> 4
+    _battery = mfr_data[8] & 0b01111111
+
+    return {
+        "sequence_number": _seq_num,
+        "dustbin_bound": _dustbin_bound,
+        "dusbin_connected": _dusbin_connected,
+        "network_connected": _network_connected,
+        "work_status": _work_status,
+        "battery": _battery,
+    }

+ 5 - 0
switchbot/const/__init__.py

@@ -67,6 +67,11 @@ class SwitchbotModel(StrEnum):
     ROLLER_SHADE = "Roller Shade"
     HUBMINI_MATTER = "HubMini Matter"
     CIRCULATOR_FAN = "Circulator Fan"
+    K20_VACUUM = "K20 Vacuum"
+    S10_VACUUM = "S10 Vacuum"
+    K10_VACUUM = "K10+ Vacuum"
+    K10_PRO_VACUUM = "K10+ Pro Vacuum"
+    K10_PRO_COMBO_VACUUM = "K10+ Pro Combo Vacuum"
 
 
 __all__ = [

+ 73 - 0
switchbot/devices/vacuum.py

@@ -0,0 +1,73 @@
+"""Library to handle connection with Switchbot."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from .device import SwitchbotSequenceDevice, update_after_operation
+
+COMMAND_CLEAN_UP = {
+    1: "570F5A00FFFF7001",
+    2: "5A400101010126",
+}
+COMMAND_RETURN_DOCK = {
+    1: "570F5A00FFFF7002",
+    2: "5A400101010225",
+}
+
+
+class SwitchbotVacuum(SwitchbotSequenceDevice):
+    """Representation of a Switchbot Vacuum."""
+
+    def __init__(self, device, password=None, interface=0, **kwargs):
+        super().__init__(device, password, interface, **kwargs)
+
+    @update_after_operation
+    async def clean_up(self, protocol_version: int) -> bool:
+        """Send command to perform a spot clean-up."""
+        return await self._send_command(COMMAND_CLEAN_UP[protocol_version])
+
+    @update_after_operation
+    async def return_to_dock(self, protocol_version: int) -> bool:
+        """Send command to return the dock."""
+        return await self._send_command(COMMAND_RETURN_DOCK[protocol_version])
+
+    async def get_basic_info(self) -> dict[str, Any] | None:
+        """Only support get the ble version through the command."""
+        if not (_data := await self._get_basic_info()):
+            return None
+        return {
+            "firmware": _data[2],
+        }
+
+    def get_soc_version(self) -> str:
+        """Return device soc version."""
+        return self._get_adv_value("soc_version")
+
+    def get_last_step(self) -> int:
+        """Return device last step after network configuration."""
+        return self._get_adv_value("step")
+
+    def get_mqtt_connnect_status(self) -> bool:
+        """Return device mqtt connect status."""
+        return self._get_adv_value("mqtt_connected")
+
+    def get_battery(self) -> int:
+        """Return device battery."""
+        return self._get_adv_value("battery")
+
+    def get_work_status(self) -> int:
+        """Return device work status."""
+        return self._get_adv_value("work_status")
+
+    def get_dustbin_bound_status(self) -> bool:
+        """Return the dustbin bound status"""
+        return self._get_adv_value("dustbin_bound")
+
+    def get_dustbin_connnected_status(self) -> bool:
+        """Return the dustbin connected status"""
+        return self._get_adv_value("dusbin_connected")
+
+    def get_network_connected_status(self) -> bool:
+        """Return the network connected status"""
+        return self._get_adv_value("network_connected")

+ 446 - 0
tests/test_adv_parser.py

@@ -2197,3 +2197,449 @@ def test_circulator_fan_with_empty_data() -> None:
         rssi=-97,
         active=True,
     )
+
+
+def test_k20_active() -> None:
+    """Test parsing k20 with active data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xb0\xe9\xfe\x01\xf3\x8f'\x01\x11S\x00\x10d\x0f"},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b".\x00d"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K20_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b".\x00d",
+            "data": {
+                "sequence_number": 39,
+                "soc_version": "1.1.083",
+                "step": 0,
+                "mqtt_connected": True,
+                "battery": 100,
+                "work_status": 15,
+            },
+            "isEncrypted": False,
+            "model": ".",
+            "modelFriendlyName": "K20 Vacuum",
+            "modelName": SwitchbotModel.K20_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_k20_passive() -> None:
+    """Test parsing k20 with passive data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xb0\xe9\xfe\x01\xf3\x8f'\x01\x11S\x00\x10d\x0f"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K20_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": None,
+            "data": {
+                "sequence_number": 39,
+                "soc_version": "1.1.083",
+                "step": 0,
+                "mqtt_connected": True,
+                "battery": 100,
+                "work_status": 15,
+            },
+            "isEncrypted": False,
+            "model": ".",
+            "modelFriendlyName": "K20 Vacuum",
+            "modelName": SwitchbotModel.K20_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=False,
+    )
+
+
+def test_k20_with_empty_data() -> None:
+    """Test parsing k20 with empty data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: None},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b".\x00d"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K20_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b".\x00d",
+            "data": {},
+            "isEncrypted": False,
+            "model": ".",
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_k10_pro_active() -> None:
+    """Test parsing k10 pro with active data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xb0\xe9\xfeP\x8d\x8d\x02 d"},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"(\x00"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(
+        ble_device, adv_data, SwitchbotModel.K10_PRO_VACUUM
+    )
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b"(\x00",
+            "data": {
+                "sequence_number": 2,
+                "dusbin_connected": False,
+                "dustbin_bound": False,
+                "network_connected": True,
+                "battery": 100,
+                "work_status": 0,
+            },
+            "isEncrypted": False,
+            "model": "(",
+            "modelFriendlyName": "K10+ Pro Vacuum",
+            "modelName": SwitchbotModel.K10_PRO_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_k10_pro_passive() -> None:
+    """Test parsing k10 pro with passive data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xb0\xe9\xfeP\x8d\x8d\x02 d"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(
+        ble_device, adv_data, SwitchbotModel.K10_PRO_VACUUM
+    )
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": None,
+            "data": {
+                "sequence_number": 2,
+                "dusbin_connected": False,
+                "dustbin_bound": False,
+                "network_connected": True,
+                "battery": 100,
+                "work_status": 0,
+            },
+            "isEncrypted": False,
+            "model": "(",
+            "modelFriendlyName": "K10+ Pro Vacuum",
+            "modelName": SwitchbotModel.K10_PRO_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=False,
+    )
+
+
+def test_k10_pro_with_empty_data() -> None:
+    """Test parsing k10 pro with empty data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: None},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"(\x00"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(
+        ble_device, adv_data, SwitchbotModel.K10_PRO_VACUUM
+    )
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b"(\x00",
+            "data": {},
+            "isEncrypted": False,
+            "model": "(",
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_k10_active() -> None:
+    """Test parsing k10+ with active data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xca8\x06\xa9_\xf1\x02 d"},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"}\x00"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K10_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b"}\x00",
+            "data": {
+                "sequence_number": 2,
+                "dusbin_connected": False,
+                "dustbin_bound": False,
+                "network_connected": True,
+                "battery": 100,
+                "work_status": 0,
+            },
+            "isEncrypted": False,
+            "model": "}",
+            "modelFriendlyName": "K10+ Vacuum",
+            "modelName": SwitchbotModel.K10_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_k10_passive() -> None:
+    """Test parsing k10+ with passive data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xca8\x06\xa9_\xf1\x02 d"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K10_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": None,
+            "data": {
+                "sequence_number": 2,
+                "dusbin_connected": False,
+                "dustbin_bound": False,
+                "network_connected": True,
+                "battery": 100,
+                "work_status": 0,
+            },
+            "isEncrypted": False,
+            "model": "}",
+            "modelFriendlyName": "K10+ Vacuum",
+            "modelName": SwitchbotModel.K10_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=False,
+    )
+
+
+def test_k10_with_empty_data() -> None:
+    """Test parsing k10+ with empty data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: None},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"}\x00"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K10_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b"}\x00",
+            "data": {},
+            "isEncrypted": False,
+            "model": "}",
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_k10_pro_combo_active() -> None:
+    """Test parsing k10+ pro combo with active data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={
+            2409: b"\xb0\xe9\xfe\x01\xf4\x1d\x0b\x01\x01\xb1\x03\x118\x01"
+        },
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"3\x00\x00"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(
+        ble_device, adv_data, SwitchbotModel.K10_PRO_COMBO_VACUUM
+    )
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b"3\x00\x00",
+            "data": {
+                "sequence_number": 11,
+                "soc_version": "1.0.945",
+                "step": 1,
+                "mqtt_connected": True,
+                "battery": 56,
+                "work_status": 1,
+            },
+            "isEncrypted": False,
+            "model": "3",
+            "modelFriendlyName": "K10+ Pro Combo Vacuum",
+            "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_k10_pro_combo_passive() -> None:
+    """Test parsing k10+ pro combo with passive data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={
+            2409: b"\xb0\xe9\xfe\x01\xf4\x1d\x0b\x01\x01\xb1\x03\x118\x01"
+        },
+        rssi=-97,
+    )
+    result = parse_advertisement_data(
+        ble_device, adv_data, SwitchbotModel.K10_PRO_COMBO_VACUUM
+    )
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": None,
+            "data": {
+                "sequence_number": 11,
+                "soc_version": "1.0.945",
+                "step": 1,
+                "mqtt_connected": True,
+                "battery": 56,
+                "work_status": 1,
+            },
+            "isEncrypted": False,
+            "model": "3",
+            "modelFriendlyName": "K10+ Pro Combo Vacuum",
+            "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=False,
+    )
+
+
+def test_k10_pro_combo_with_empty_data() -> None:
+    """Test parsing k10+ pro combo with empty data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: None},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"3\x00\x00"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(
+        ble_device, adv_data, SwitchbotModel.K10_PRO_COMBO_VACUUM
+    )
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b"3\x00\x00",
+            "data": {},
+            "isEncrypted": False,
+            "model": "3",
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_s10_active() -> None:
+    """Test parsing s10 with active data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xb0\xe9\xfe\x00\x08|\n\x01\x11\x05\x00\x10M\x02"},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"z\x00\x00"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.S10_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b"z\x00\x00",
+            "data": {
+                "sequence_number": 10,
+                "soc_version": "1.1.005",
+                "step": 0,
+                "mqtt_connected": True,
+                "battery": 77,
+                "work_status": 2,
+            },
+            "isEncrypted": False,
+            "model": "z",
+            "modelFriendlyName": "S10 Vacuum",
+            "modelName": SwitchbotModel.S10_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+def test_s10_passive() -> None:
+    """Test parsing s10 with passive data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xb0\xe9\xfe\x00\x08|\n\x01\x11\x05\x00\x10M\x02"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.S10_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": None,
+            "data": {
+                "sequence_number": 10,
+                "soc_version": "1.1.005",
+                "step": 0,
+                "mqtt_connected": True,
+                "battery": 77,
+                "work_status": 2,
+            },
+            "isEncrypted": False,
+            "model": "z",
+            "modelFriendlyName": "S10 Vacuum",
+            "modelName": SwitchbotModel.S10_VACUUM,
+        },
+        device=ble_device,
+        rssi=-97,
+        active=False,
+    )
+
+
+def test_s10_with_empty_data() -> None:
+    """Test parsing s10 with empty data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: None},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"z\x00\x00"},
+        rssi=-97,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.S10_VACUUM)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": b"z\x00\x00",
+            "data": {},
+            "isEncrypted": False,
+            "model": "z",
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )

+ 135 - 0
tests/test_vacuum.py

@@ -0,0 +1,135 @@
+from unittest.mock import AsyncMock
+
+import pytest
+from bleak.backends.device import BLEDevice
+
+from switchbot import SwitchBotAdvertisement
+from switchbot.adv_parser import SUPPORTED_TYPES
+from switchbot.devices import vacuum
+
+from .test_adv_parser import generate_ble_device
+
+common_params = [
+    (b".\x00d", ".", 2),
+    (b"z\x00\x00", ".", 2),
+    (b"3\x00\x00", ".", 2),
+    (b"(\x00", "(", 1),
+    (b"}\x00", "(", 1),
+]
+
+
+def create_device_for_command_testing(
+    protocol_version: int, rawAdvData: bytes, model: str
+):
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    device = vacuum.SwitchbotVacuum(ble_device)
+    device.update_from_advertisement(
+        make_advertisement_data(ble_device, protocol_version, rawAdvData, model)
+    )
+    device._send_command = AsyncMock()
+    device.update = AsyncMock()
+    return device
+
+
+def make_advertisement_data(
+    ble_device: BLEDevice, protocol_version: int, rawAdvData: bytes, model: str
+):
+    """Set advertisement data with defaults."""
+    if protocol_version == 1:
+        return SwitchBotAdvertisement(
+            address="aa:bb:cc:dd:ee:ff",
+            data={
+                "rawAdvData": rawAdvData,
+                "data": {
+                    "sequence_number": 2,
+                    "dusbin_connected": False,
+                    "dustbin_bound": False,
+                    "network_connected": True,
+                    "battery": 100,
+                    "work_status": 0,
+                },
+                "isEncrypted": False,
+                "model": model,
+                "modelFriendlyName": SUPPORTED_TYPES[model]["modelFriendlyName"],
+                "modelName": SUPPORTED_TYPES[model]["modelName"],
+            },
+            device=ble_device,
+            rssi=-97,
+            active=True,
+        )
+    return SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": rawAdvData,
+            "data": {
+                "soc_version": "1.1.083",
+                "step": 0,
+                "mqtt_connected": True,
+                "battery": 100,
+                "work_status": 15,
+            },
+            "isEncrypted": False,
+            "model": model,
+            "modelFriendlyName": SUPPORTED_TYPES[model]["modelFriendlyName"],
+            "modelName": SUPPORTED_TYPES[model]["modelName"],
+        },
+        device=ble_device,
+        rssi=-97,
+        active=True,
+    )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    ("rawAdvData", "model"),
+    [(b".\x00d", "."), (b"z\x00\x00", "z"), (b"3\x00\x00", "3")],
+)
+async def test_status_from_proceess_adv(rawAdvData, model, protocol_version=2):
+    device = create_device_for_command_testing(protocol_version, rawAdvData, model)
+    assert device.get_soc_version() == "1.1.083"
+    assert device.get_last_step() == 0
+    assert device.get_mqtt_connnect_status() is True
+    assert device.get_battery() == 100
+    assert device.get_work_status() == 15
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(("rawAdvData", "model"), [(b"(\x00", "("), (b"}\x00", "}")])
+async def test_status_from_proceess_adv_k(rawAdvData, model, protocol_version=1):
+    device = create_device_for_command_testing(protocol_version, rawAdvData, model)
+    assert device.get_dustbin_bound_status() is False
+    assert device.get_dustbin_connnected_status() is False
+    assert device.get_network_connected_status() is True
+    assert device.get_battery() == 100
+    assert device.get_work_status() == 0
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(("rawAdvData", "model", "protocol_version"), common_params)
+async def test_clean_up(rawAdvData, model, protocol_version):
+    device = create_device_for_command_testing(protocol_version, rawAdvData, model)
+    await device.clean_up(protocol_version)
+    device._send_command.assert_awaited_once_with(
+        vacuum.COMMAND_CLEAN_UP[protocol_version]
+    )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(("rawAdvData", "model", "protocol_version"), common_params)
+async def test_return_to_dock(rawAdvData, model, protocol_version):
+    device = create_device_for_command_testing(protocol_version, rawAdvData, model)
+    await device.return_to_dock(protocol_version)
+    device._send_command.assert_awaited_once_with(
+        vacuum.COMMAND_RETURN_DOCK[protocol_version]
+    )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(("rawAdvData", "model", "protocol_version"), common_params)
+async def test_get_basic_info_returns_none_when_no_data(
+    rawAdvData, model, protocol_version
+):
+    device = create_device_for_command_testing(protocol_version, rawAdvData, model)
+    device._get_basic_info = AsyncMock(return_value=None)
+
+    assert await device.get_basic_info() is None