Browse Source

refactor: replace model-default override boilerplate with `_model` classvar (#476)

J. Nick Koston 1 day ago
parent
commit
e75f8c71ca

+ 1 - 26
switchbot/devices/air_purifier.py

@@ -5,8 +5,6 @@ from __future__ import annotations
 import logging
 import logging
 from typing import Any, ClassVar
 from typing import Any, ClassVar
 
 
-from bleak.backends.device import BLEDevice
-
 from ..adv_parsers.air_purifier import get_air_purifier_mode
 from ..adv_parsers.air_purifier import get_air_purifier_mode
 from ..const import SwitchbotModel
 from ..const import SwitchbotModel
 from ..const.air_purifier import AirPurifierMode, AirQualityLevel
 from ..const.air_purifier import AirPurifierMode, AirQualityLevel
@@ -40,6 +38,7 @@ READ_LED_STATUS_COMMAND = "570f4d07"
 class SwitchbotAirPurifier(SwitchbotSequenceBaseLight, SwitchbotEncryptedDevice):
 class SwitchbotAirPurifier(SwitchbotSequenceBaseLight, SwitchbotEncryptedDevice):
     """Representation of a Switchbot Air Purifier."""
     """Representation of a Switchbot Air Purifier."""
 
 
+    _model = SwitchbotModel.AIR_PURIFIER_US
     _turn_on_command = f"{COMMAND_HEAD}010100"
     _turn_on_command = f"{COMMAND_HEAD}010100"
     _turn_off_command = f"{COMMAND_HEAD}010000"
     _turn_off_command = f"{COMMAND_HEAD}010000"
     _open_child_lock_command = f"{COMMAND_HEAD}0301"
     _open_child_lock_command = f"{COMMAND_HEAD}0301"
@@ -78,30 +77,6 @@ class SwitchbotAirPurifier(SwitchbotSequenceBaseLight, SwitchbotEncryptedDevice)
         }
         }
     )
     )
 
 
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER_US,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
-
-    @classmethod
-    async def verify_encryption_key(
-        cls,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER_US,
-        **kwargs: Any,
-    ) -> bool:
-        return await super().verify_encryption_key(
-            device, key_id, encryption_key, model, **kwargs
-        )
-
     @property
     @property
     def color_modes(self) -> set[ColorMode]:
     def color_modes(self) -> set[ColorMode]:
         """Return the supported color modes."""
         """Return the supported color modes."""

+ 2 - 26
switchbot/devices/art_frame.py

@@ -3,8 +3,6 @@
 import logging
 import logging
 from typing import Any
 from typing import Any
 
 
-from bleak.backends.device import BLEDevice
-
 from ..const import SwitchbotModel
 from ..const import SwitchbotModel
 from .device import (
 from .device import (
     SwitchbotEncryptedDevice,
     SwitchbotEncryptedDevice,
@@ -20,30 +18,8 @@ COMMAND_SET_IMAGE = "570F7A02{}"
 class SwitchbotArtFrame(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
 class SwitchbotArtFrame(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
     """Representation of a Switchbot Art Frame."""
     """Representation of a Switchbot Art Frame."""
 
 
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.ART_FRAME,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
-        self.response_flag = True
-
-    @classmethod
-    async def verify_encryption_key(
-        cls,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        model: SwitchbotModel = SwitchbotModel.ART_FRAME,
-        **kwargs: Any,
-    ) -> bool:
-        return await super().verify_encryption_key(
-            device, key_id, encryption_key, model, **kwargs
-        )
+    _model = SwitchbotModel.ART_FRAME
+    response_flag: bool = True
 
 
     async def get_basic_info(self) -> dict[str, Any] | None:
     async def get_basic_info(self) -> dict[str, Any] | None:
         """Get device basic settings."""
         """Get device basic settings."""

+ 17 - 3
switchbot/devices/device.py

@@ -996,16 +996,22 @@ class SwitchbotDevice(SwitchbotBaseDevice):
 class SwitchbotEncryptedDevice(SwitchbotDevice):
 class SwitchbotEncryptedDevice(SwitchbotDevice):
     """A Switchbot device that uses encryption."""
     """A Switchbot device that uses encryption."""
 
 
+    _model: SwitchbotModel | None = None
+
     def __init__(
     def __init__(
         self,
         self,
         device: BLEDevice,
         device: BLEDevice,
         key_id: str,
         key_id: str,
         encryption_key: str,
         encryption_key: str,
-        model: SwitchbotModel,
         interface: int = 0,
         interface: int = 0,
+        model: SwitchbotModel | None = None,
         **kwargs: Any,
         **kwargs: Any,
     ) -> None:
     ) -> None:
         """Switchbot base class constructor for encrypted devices."""
         """Switchbot base class constructor for encrypted devices."""
+        if model is None:
+            model = self._model
+        if model is None:
+            raise ValueError("model must be provided or set on the subclass as _model")
         if len(key_id) == 0:
         if len(key_id) == 0:
             raise ValueError("key_id is missing")
             raise ValueError("key_id is missing")
         if len(key_id) != 2:
         if len(key_id) != 2:
@@ -1080,12 +1086,20 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
         device: BLEDevice,
         device: BLEDevice,
         key_id: str,
         key_id: str,
         encryption_key: str,
         encryption_key: str,
-        model: SwitchbotModel,
+        model: SwitchbotModel | None = None,
         **kwargs: Any,
         **kwargs: Any,
     ) -> bool:
     ) -> bool:
+        if model is None:
+            model = cls._model
+        if model is None:
+            raise ValueError("model must be provided or set on the subclass as _model")
         try:
         try:
             switchbot_device = cls(
             switchbot_device = cls(
-                device, key_id=key_id, encryption_key=encryption_key, model=model
+                device,
+                key_id=key_id,
+                encryption_key=encryption_key,
+                model=model,
+                **kwargs,
             )
             )
         except ValueError:
         except ValueError:
             return False
             return False

+ 2 - 27
switchbot/devices/evaporative_humidifier.py

@@ -1,8 +1,6 @@
 import logging
 import logging
 from typing import Any
 from typing import Any
 
 
-from bleak.backends.device import BLEDevice
-
 from ..adv_parsers.humidifier import calculate_temperature_and_humidity
 from ..adv_parsers.humidifier import calculate_temperature_and_humidity
 from ..const import SwitchbotModel
 from ..const import SwitchbotModel
 from ..const.evaporative_humidifier import (
 from ..const.evaporative_humidifier import (
@@ -47,33 +45,10 @@ DEVICE_GET_BASIC_SETTINGS_KEY = "570f4481"
 class SwitchbotEvaporativeHumidifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
 class SwitchbotEvaporativeHumidifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
     """Representation of a Switchbot Evaporative Humidifier"""
     """Representation of a Switchbot Evaporative Humidifier"""
 
 
+    _model = SwitchbotModel.EVAPORATIVE_HUMIDIFIER
     _turn_on_command = COMMAND_TURN_ON
     _turn_on_command = COMMAND_TURN_ON
     _turn_off_command = f"{COMMAND_HEADER}0f430100"
     _turn_off_command = f"{COMMAND_HEADER}0f430100"
-
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
-        **kwargs: Any,
-    ) -> None:
-        self._force_next_update = False
-        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
-
-    @classmethod
-    async def verify_encryption_key(
-        cls,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        model: SwitchbotModel = SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
-        **kwargs: Any,
-    ) -> bool:
-        return await super().verify_encryption_key(
-            device, key_id, encryption_key, model, **kwargs
-        )
+    _force_next_update: bool = False
 
 
     async def get_basic_info(self) -> dict[str, Any] | None:
     async def get_basic_info(self) -> dict[str, Any] | None:
         """Get device basic settings."""
         """Get device basic settings."""

+ 1 - 1
switchbot/devices/keypad_vision.py

@@ -27,7 +27,7 @@ class SwitchbotKeypadVision(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
         **kwargs: Any,
         **kwargs: Any,
     ) -> None:
     ) -> None:
         """Initialize Keypad Vision (Pro) device."""
         """Initialize Keypad Vision (Pro) device."""
-        super().__init__(device, key_id, encryption_key, model, **kwargs)
+        super().__init__(device, key_id, encryption_key, model=model, **kwargs)
 
 
     @classmethod
     @classmethod
     async def verify_encryption_key(
     async def verify_encryption_key(

+ 2 - 49
switchbot/devices/light_strip.py

@@ -2,8 +2,6 @@ from __future__ import annotations
 
 
 from typing import Any
 from typing import Any
 
 
-from bleak.backends.device import BLEDevice
-
 from ..const import SwitchbotModel
 from ..const import SwitchbotModel
 from ..const.light import ColorMode, RGBICStripLightColorMode, StripLightColorMode
 from ..const.light import ColorMode, RGBICStripLightColorMode, StripLightColorMode
 from .base_light import SwitchbotSequenceBaseLight
 from .base_light import SwitchbotSequenceBaseLight
@@ -274,29 +272,7 @@ class SwitchbotLightStrip(SwitchbotSequenceBaseLight):
 class SwitchbotStripLight3(SwitchbotEncryptedDevice, SwitchbotLightStrip):
 class SwitchbotStripLight3(SwitchbotEncryptedDevice, SwitchbotLightStrip):
     """Support for switchbot strip light3 and floor lamp."""
     """Support for switchbot strip light3 and floor lamp."""
 
 
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.STRIP_LIGHT_3,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
-
-    @classmethod
-    async def verify_encryption_key(
-        cls,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        model: SwitchbotModel = SwitchbotModel.STRIP_LIGHT_3,
-        **kwargs: Any,
-    ) -> bool:
-        return await super().verify_encryption_key(
-            device, key_id, encryption_key, model, **kwargs
-        )
+    _model = SwitchbotModel.STRIP_LIGHT_3
 
 
     @property
     @property
     def color_modes(self) -> set[ColorMode]:
     def color_modes(self) -> set[ColorMode]:
@@ -307,32 +283,9 @@ class SwitchbotStripLight3(SwitchbotEncryptedDevice, SwitchbotLightStrip):
 class SwitchbotRgbicLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
 class SwitchbotRgbicLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
     """Support for Switchbot RGBIC lights."""
     """Support for Switchbot RGBIC lights."""
 
 
+    _model = SwitchbotModel.RGBICWW_STRIP_LIGHT
     _effect_dict = RGBIC_EFFECTS
     _effect_dict = RGBIC_EFFECTS
 
 
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.RGBICWW_STRIP_LIGHT,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
-
-    @classmethod
-    async def verify_encryption_key(
-        cls,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        model: SwitchbotModel = SwitchbotModel.RGBICWW_STRIP_LIGHT,
-        **kwargs: Any,
-    ) -> bool:
-        return await super().verify_encryption_key(
-            device, key_id, encryption_key, model, **kwargs
-        )
-
     @property
     @property
     def color_modes(self) -> set[ColorMode]:
     def color_modes(self) -> set[ColorMode]:
         """Return the supported color modes."""
         """Return the supported color modes."""

+ 7 - 16
switchbot/devices/lock.py

@@ -83,15 +83,20 @@ COMMAND_RESULT_EXPECTED_VALUES = {1, 6}
 class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
 class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
     """Representation of a Switchbot Lock."""
     """Representation of a Switchbot Lock."""
 
 
+    _model = SwitchbotModel.LOCK
+    _notifications_enabled: bool = False
+
     def __init__(
     def __init__(
         self,
         self,
         device: BLEDevice,
         device: BLEDevice,
         key_id: str,
         key_id: str,
         encryption_key: str,
         encryption_key: str,
         interface: int = 0,
         interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.LOCK,
+        model: SwitchbotModel | None = None,
         **kwargs: Any,
         **kwargs: Any,
     ) -> None:
     ) -> None:
+        if model is None:
+            model = self._model
         if model not in (
         if model not in (
             SwitchbotModel.LOCK,
             SwitchbotModel.LOCK,
             SwitchbotModel.LOCK_PRO,
             SwitchbotModel.LOCK_PRO,
@@ -102,21 +107,7 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
             SwitchbotModel.LOCK_PRO_WIFI,
             SwitchbotModel.LOCK_PRO_WIFI,
         ):
         ):
             raise ValueError("initializing SwitchbotLock with a non-lock model")
             raise ValueError("initializing SwitchbotLock with a non-lock model")
-        self._notifications_enabled: bool = False
-        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
-
-    @classmethod
-    async def verify_encryption_key(
-        cls,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        model: SwitchbotModel = SwitchbotModel.LOCK,
-        **kwargs: Any,
-    ) -> bool:
-        return await super().verify_encryption_key(
-            device, key_id, encryption_key, model, **kwargs
-        )
+        super().__init__(device, key_id, encryption_key, interface, model, **kwargs)
 
 
     async def lock(self) -> bool:
     async def lock(self) -> bool:
         """Send lock command."""
         """Send lock command."""

+ 4 - 48
switchbot/devices/relay_switch.py

@@ -2,8 +2,6 @@ import logging
 import time
 import time
 from typing import Any
 from typing import Any
 
 
-from bleak.backends.device import BLEDevice
-
 from ..const import SwitchbotModel
 from ..const import SwitchbotModel
 from ..helpers import parse_power_data, parse_uint24_be
 from ..helpers import parse_power_data, parse_uint24_be
 from ..models import SwitchBotAdvertisement
 from ..models import SwitchBotAdvertisement
@@ -59,33 +57,10 @@ MULTI_CHANNEL_COMMANDS_GET_VOLTAGE_AND_CURRENT = {
 class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
 class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
     """Representation of a Switchbot relay switch 1pm."""
     """Representation of a Switchbot relay switch 1pm."""
 
 
+    _model = SwitchbotModel.RELAY_SWITCH_1PM
     _turn_on_command = f"{COMMAND_CONTROL}010100"
     _turn_on_command = f"{COMMAND_CONTROL}010100"
     _turn_off_command = f"{COMMAND_CONTROL}010000"
     _turn_off_command = f"{COMMAND_CONTROL}010000"
 
 
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.RELAY_SWITCH_1PM,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
-
-    @classmethod
-    async def verify_encryption_key(
-        cls,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        model: SwitchbotModel = SwitchbotModel.RELAY_SWITCH_1PM,
-        **kwargs: Any,
-    ) -> bool:
-        return await super().verify_encryption_key(
-            device, key_id, encryption_key, model, **kwargs
-        )
-
     def _reset_power_data(self, data: dict[str, Any]) -> None:
     def _reset_power_data(self, data: dict[str, Any]) -> None:
         """Reset power-related data to 0."""
         """Reset power-related data to 0."""
         for key in ["power", "current", "voltage"]:
         for key in ["power", "current", "voltage"]:
@@ -213,36 +188,17 @@ class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
 class SwitchbotGarageDoorOpener(SwitchbotRelaySwitch):
 class SwitchbotGarageDoorOpener(SwitchbotRelaySwitch):
     """Representation of a Switchbot garage door opener."""
     """Representation of a Switchbot garage door opener."""
 
 
+    _model = SwitchbotModel.GARAGE_DOOR_OPENER
     _open_command = f"{COMMAND_CONTROL}110129"
     _open_command = f"{COMMAND_CONTROL}110129"
     _close_command = f"{COMMAND_CONTROL}110229"
     _close_command = f"{COMMAND_CONTROL}110229"
     _press_command = f"{COMMAND_CONTROL}110329"  # for garage door opener toggle
     _press_command = f"{COMMAND_CONTROL}110329"  # for garage door opener toggle
 
 
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.GARAGE_DOOR_OPENER,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(device, key_id, encryption_key, interface, model, **kwargs)
-
 
 
 class SwitchbotRelaySwitch2PM(SwitchbotRelaySwitch):
 class SwitchbotRelaySwitch2PM(SwitchbotRelaySwitch):
     """Representation of a Switchbot relay switch 2pm."""
     """Representation of a Switchbot relay switch 2pm."""
 
 
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.RELAY_SWITCH_2PM,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(device, key_id, encryption_key, interface, model, **kwargs)
-        self._channel = 2
+    _model = SwitchbotModel.RELAY_SWITCH_2PM
+    _channel: int = 2
 
 
     @property
     @property
     def channel(self) -> int:
     def channel(self) -> int:

+ 1 - 26
switchbot/devices/smart_thermostat_radiator.py

@@ -3,8 +3,6 @@
 import logging
 import logging
 from typing import Any
 from typing import Any
 
 
-from bleak.backends.device import BLEDevice
-
 from ..const import SwitchbotModel
 from ..const import SwitchbotModel
 from ..const.climate import ClimateAction, ClimateMode
 from ..const.climate import ClimateAction, ClimateMode
 from ..const.climate import SmartThermostatRadiatorMode as STRMode
 from ..const.climate import SmartThermostatRadiatorMode as STRMode
@@ -50,33 +48,10 @@ class SwitchbotSmartThermostatRadiator(
 ):
 ):
     """Representation of a Switchbot Smart Thermostat Radiator."""
     """Representation of a Switchbot Smart Thermostat Radiator."""
 
 
+    _model = SwitchbotModel.SMART_THERMOSTAT_RADIATOR
     _turn_off_command = "570100"
     _turn_off_command = "570100"
     _turn_on_command = "570101"
     _turn_on_command = "570101"
 
 
-    def __init__(
-        self,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        interface: int = 0,
-        model: SwitchbotModel = SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
-
-    @classmethod
-    async def verify_encryption_key(
-        cls,
-        device: BLEDevice,
-        key_id: str,
-        encryption_key: str,
-        model: SwitchbotModel = SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
-        **kwargs: Any,
-    ) -> bool:
-        return await super().verify_encryption_key(
-            device, key_id, encryption_key, model, **kwargs
-        )
-
     @property
     @property
     def min_temperature(self) -> float:
     def min_temperature(self) -> float:
         """Return the minimum target temperature."""
         """Return the minimum target temperature."""

+ 5 - 23
tests/test_air_purifier.py

@@ -1,11 +1,10 @@
-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import AsyncMock, MagicMock
 
 
 import pytest
 import pytest
 from bleak.backends.device import BLEDevice
 from bleak.backends.device import BLEDevice
 
 
 from switchbot import (
 from switchbot import (
     SwitchBotAdvertisement,
     SwitchBotAdvertisement,
-    SwitchbotEncryptedDevice,
     SwitchbotModel,
     SwitchbotModel,
     SwitchbotOperationError,
     SwitchbotOperationError,
 )
 )
@@ -254,29 +253,12 @@ async def test_get_basic_info(device_case, info_case):
     assert info["light_sensitive"] == result[10]
     assert info["light_sensitive"] == result[10]
 
 
 
 
-@pytest.mark.asyncio
-@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
-async def test_verify_encryption_key(mock_parent_verify):
+def test_default_model_classvar():
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
-    key_id = "ff"
-    encryption_key = "ffffffffffffffffffffffffffffffff"
-
-    mock_parent_verify.return_value = True
-
-    result = await air_purifier.SwitchbotAirPurifier.verify_encryption_key(
-        device=ble_device,
-        key_id=key_id,
-        encryption_key=encryption_key,
-    )
-
-    mock_parent_verify.assert_awaited_once_with(
-        ble_device,
-        key_id,
-        encryption_key,
-        SwitchbotModel.AIR_PURIFIER_US,
+    device = air_purifier.SwitchbotAirPurifier(
+        ble_device, "ff", "ffffffffffffffffffffffffffffffff"
     )
     )
-
-    assert result is True
+    assert device._model == SwitchbotModel.AIR_PURIFIER_US
 
 
 
 
 def test_get_modes():
 def test_get_modes():

+ 3 - 24
tests/test_art_frame.py

@@ -5,7 +5,6 @@ from bleak.backends.device import BLEDevice
 
 
 from switchbot import SwitchBotAdvertisement
 from switchbot import SwitchBotAdvertisement
 from switchbot.devices.art_frame import COMMAND_SET_IMAGE, SwitchbotArtFrame
 from switchbot.devices.art_frame import COMMAND_SET_IMAGE, SwitchbotArtFrame
-from switchbot.devices.device import SwitchbotEncryptedDevice
 
 
 from . import ART_FRAME_INFO
 from . import ART_FRAME_INFO
 from .test_adv_parser import AdvTestCase, generate_ble_device
 from .test_adv_parser import AdvTestCase, generate_ble_device
@@ -195,27 +194,7 @@ async def test_set_image_with_valid_index() -> None:
         device._send_command.assert_awaited_with(COMMAND_SET_IMAGE.format("14"))
         device._send_command.assert_awaited_with(COMMAND_SET_IMAGE.format("14"))
 
 
 
 
-@pytest.mark.asyncio
-@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
-async def test_verify_encryption_key(mock_parent_verify: AsyncMock) -> None:
+def test_default_model_classvar() -> None:
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
-    key_id = "ff"
-    encryption_key = "ffffffffffffffffffffffffffffffff"
-
-    mock_parent_verify.return_value = True
-
-    result = await SwitchbotArtFrame.verify_encryption_key(
-        device=ble_device,
-        key_id=key_id,
-        encryption_key=encryption_key,
-        model=ART_FRAME_INFO.modelName,
-    )
-
-    mock_parent_verify.assert_awaited_once_with(
-        ble_device,
-        key_id,
-        encryption_key,
-        ART_FRAME_INFO.modelName,
-    )
-
-    assert result is True
+    device = SwitchbotArtFrame(ble_device, "ff", "ffffffffffffffffffffffffffffffff")
+    assert device._model == ART_FRAME_INFO.modelName

+ 49 - 4
tests/test_encrypted_device.py

@@ -56,22 +56,30 @@ async def test_encrypted_device_init_validation() -> None:
     # Test empty key_id
     # Test empty key_id
     with pytest.raises(ValueError, match="key_id is missing"):
     with pytest.raises(ValueError, match="key_id is missing"):
         MockEncryptedDevice(
         MockEncryptedDevice(
-            ble_device, "", "0123456789abcdef0123456789abcdef", SwitchbotModel.LOCK
+            ble_device,
+            "",
+            "0123456789abcdef0123456789abcdef",
+            model=SwitchbotModel.LOCK,
         )
         )
 
 
     # Test invalid key_id length
     # Test invalid key_id length
     with pytest.raises(ValueError, match="key_id is invalid"):
     with pytest.raises(ValueError, match="key_id is invalid"):
         MockEncryptedDevice(
         MockEncryptedDevice(
-            ble_device, "1", "0123456789abcdef0123456789abcdef", SwitchbotModel.LOCK
+            ble_device,
+            "1",
+            "0123456789abcdef0123456789abcdef",
+            model=SwitchbotModel.LOCK,
         )
         )
 
 
     # Test empty encryption_key
     # Test empty encryption_key
     with pytest.raises(ValueError, match="encryption_key is missing"):
     with pytest.raises(ValueError, match="encryption_key is missing"):
-        MockEncryptedDevice(ble_device, "01", "", SwitchbotModel.LOCK)
+        MockEncryptedDevice(ble_device, "01", "", model=SwitchbotModel.LOCK)
 
 
     # Test invalid encryption_key length
     # Test invalid encryption_key length
     with pytest.raises(ValueError, match="encryption_key is invalid"):
     with pytest.raises(ValueError, match="encryption_key is invalid"):
-        MockEncryptedDevice(ble_device, "01", "0123456789abcdef", SwitchbotModel.LOCK)
+        MockEncryptedDevice(
+            ble_device, "01", "0123456789abcdef", model=SwitchbotModel.LOCK
+        )
 
 
 
 
 @pytest.mark.asyncio
 @pytest.mark.asyncio
@@ -529,6 +537,43 @@ async def test_empty_data_encryption_decryption() -> None:
     assert decrypted == b""
     assert decrypted == b""
 
 
 
 
+@pytest.mark.asyncio
+async def test_verify_encryption_key_falls_back_to_classvar() -> None:
+    """verify_encryption_key resolves `model` from `cls._model` when omitted."""
+
+    class ClassvarModelDevice(MockEncryptedDevice):
+        _model = SwitchbotModel.LOCK
+
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Test Device")
+    with patch.object(
+        ClassvarModelDevice,
+        "get_basic_info",
+        new_callable=AsyncMock,
+        return_value={"ok": True},
+    ):
+        result = await ClassvarModelDevice.verify_encryption_key(
+            ble_device, "01", "0123456789abcdef0123456789abcdef"
+        )
+    assert result is True
+
+
+@pytest.mark.asyncio
+async def test_verify_encryption_key_without_model_or_classvar_raises() -> None:
+    """verify_encryption_key raises when neither `model=` nor `cls._model` is set."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Test Device")
+    with pytest.raises(ValueError, match="model must be provided"):
+        await MockEncryptedDevice.verify_encryption_key(
+            ble_device, "01", "0123456789abcdef0123456789abcdef"
+        )
+
+
+def test_init_without_model_or_classvar_raises() -> None:
+    """__init__ raises when neither `model=` nor `cls._model` is set."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Test Device")
+    with pytest.raises(ValueError, match="model must be provided"):
+        MockEncryptedDevice(ble_device, "01", "0123456789abcdef0123456789abcdef")
+
+
 @pytest.mark.asyncio
 @pytest.mark.asyncio
 async def test_decrypt_with_none_iv_during_disconnect() -> None:
 async def test_decrypt_with_none_iv_during_disconnect() -> None:
     """Test that decryption returns empty bytes when IV is None during expected disconnect."""
     """Test that decryption returns empty bytes when IV is None during expected disconnect."""

+ 6 - 23
tests/test_evaporative_humidifier.py

@@ -1,5 +1,5 @@
 import datetime
 import datetime
-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import AsyncMock, MagicMock
 
 
 import pytest
 import pytest
 from bleak.backends.device import BLEDevice
 from bleak.backends.device import BLEDevice
@@ -12,7 +12,7 @@ from switchbot import (
     SwitchbotModel,
     SwitchbotModel,
 )
 )
 from switchbot.devices import evaporative_humidifier
 from switchbot.devices import evaporative_humidifier
-from switchbot.devices.device import SwitchbotEncryptedDevice, SwitchbotOperationError
+from switchbot.devices.device import SwitchbotOperationError
 
 
 from .test_adv_parser import generate_ble_device
 from .test_adv_parser import generate_ble_device
 
 
@@ -311,29 +311,12 @@ async def test_set_child_lock(enabled, command):
     device._send_command.assert_awaited_once_with(command)
     device._send_command.assert_awaited_once_with(command)
 
 
 
 
-@pytest.mark.asyncio
-@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
-async def test_verify_encryption_key(mock_parent_verify):
+def test_default_model_classvar():
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
-    key_id = "ff"
-    encryption_key = "ffffffffffffffffffffffffffffffff"
-
-    mock_parent_verify.return_value = True
-
-    result = await evaporative_humidifier.SwitchbotEvaporativeHumidifier.verify_encryption_key(
-        device=ble_device,
-        key_id=key_id,
-        encryption_key=encryption_key,
+    device = evaporative_humidifier.SwitchbotEvaporativeHumidifier(
+        ble_device, "ff", "ffffffffffffffffffffffffffffffff"
     )
     )
-
-    mock_parent_verify.assert_awaited_once_with(
-        ble_device,
-        key_id,
-        encryption_key,
-        SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
-    )
-
-    assert result is True
+    assert device._model == SwitchbotModel.EVAPORATIVE_HUMIDIFIER
 
 
 
 
 def test_evaporative_humidifier_modes():
 def test_evaporative_humidifier_modes():

+ 4 - 21
tests/test_lock.py

@@ -50,28 +50,11 @@ def test_lock_init_with_invalid_model(model: str):
         create_device_for_command_testing(model)
         create_device_for_command_testing(model)
 
 
 
 
-@pytest.mark.asyncio
-@pytest.mark.parametrize(
-    "model",
-    [
-        SwitchbotModel.LOCK,
-        SwitchbotModel.LOCK_LITE,
-        SwitchbotModel.LOCK_PRO,
-        SwitchbotModel.LOCK_ULTRA,
-        SwitchbotModel.LOCK_VISION,
-        SwitchbotModel.LOCK_VISION_PRO,
-        SwitchbotModel.LOCK_PRO_WIFI,
-    ],
-)
-async def test_verify_encryption_key(model: str):
-    """Test verify_encryption_key method."""
+def test_default_model_classvar():
+    """The classvar default is SwitchbotModel.LOCK."""
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
-    with patch("switchbot.devices.lock.super") as mock_super:
-        mock_super().verify_encryption_key = AsyncMock(return_value=True)
-        result = await lock.SwitchbotLock.verify_encryption_key(
-            ble_device, "key_id", "encryption_key", model
-        )
-        assert result is True
+    device = lock.SwitchbotLock(ble_device, "ff", "ffffffffffffffffffffffffffffffff")
+    assert device._model == SwitchbotModel.LOCK
 
 
 
 
 @pytest.mark.asyncio
 @pytest.mark.asyncio

+ 9 - 30
tests/test_relay_switch.py

@@ -1,9 +1,9 @@
-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import AsyncMock, MagicMock
 
 
 import pytest
 import pytest
 from bleak.backends.device import BLEDevice
 from bleak.backends.device import BLEDevice
 
 
-from switchbot import SwitchBotAdvertisement, SwitchbotEncryptedDevice, SwitchbotModel
+from switchbot import SwitchBotAdvertisement, SwitchbotModel
 from switchbot.devices import relay_switch
 from switchbot.devices import relay_switch
 from switchbot.devices.device import _merge_data as merge_data
 from switchbot.devices.device import _merge_data as merge_data
 
 
@@ -393,39 +393,18 @@ async def test_get_basic_info_garage_door_opener(rawAdvData, model, info_data):
     assert info["door_open"] is True
     assert info["door_open"] is True
 
 
 
 
-@pytest.mark.asyncio
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
-    "model",
+    ("dev_cls", "expected_model"),
     [
     [
-        SwitchbotModel.RELAY_SWITCH_1,
-        SwitchbotModel.RELAY_SWITCH_1PM,
-        SwitchbotModel.GARAGE_DOOR_OPENER,
-        SwitchbotModel.RELAY_SWITCH_2PM,
+        (relay_switch.SwitchbotRelaySwitch, SwitchbotModel.RELAY_SWITCH_1PM),
+        (relay_switch.SwitchbotGarageDoorOpener, SwitchbotModel.GARAGE_DOOR_OPENER),
+        (relay_switch.SwitchbotRelaySwitch2PM, SwitchbotModel.RELAY_SWITCH_2PM),
     ],
     ],
 )
 )
-@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
-async def test_verify_encryption_key(mock_parent_verify, model):
+def test_default_model_classvar(dev_cls, expected_model):
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
-    key_id = "ff"
-    encryption_key = "ffffffffffffffffffffffffffffffff"
-
-    mock_parent_verify.return_value = True
-
-    result = await relay_switch.SwitchbotRelaySwitch.verify_encryption_key(
-        device=ble_device,
-        key_id=key_id,
-        encryption_key=encryption_key,
-        model=model,
-    )
-
-    mock_parent_verify.assert_awaited_once_with(
-        ble_device,
-        key_id,
-        encryption_key,
-        model,
-    )
-
-    assert result is True
+    device = dev_cls(ble_device, "ff", "ffffffffffffffffffffffffffffffff")
+    assert device._model == expected_model
 
 
 
 
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(

+ 6 - 24
tests/test_smart_thermostat_radiator.py

@@ -1,4 +1,4 @@
-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import AsyncMock, MagicMock
 
 
 import pytest
 import pytest
 from bleak.backends.device import BLEDevice
 from bleak.backends.device import BLEDevice
@@ -6,7 +6,7 @@ from bleak.backends.device import BLEDevice
 from switchbot import SwitchBotAdvertisement
 from switchbot import SwitchBotAdvertisement
 from switchbot.const.climate import ClimateAction, ClimateMode
 from switchbot.const.climate import ClimateAction, ClimateMode
 from switchbot.const.climate import SmartThermostatRadiatorMode as STRMode
 from switchbot.const.climate import SmartThermostatRadiatorMode as STRMode
-from switchbot.devices.device import SwitchbotEncryptedDevice, SwitchbotOperationError
+from switchbot.devices.device import SwitchbotOperationError
 from switchbot.devices.smart_thermostat_radiator import (
 from switchbot.devices.smart_thermostat_radiator import (
     COMMAND_SET_MODE,
     COMMAND_SET_MODE,
     COMMAND_SET_TEMP,
     COMMAND_SET_TEMP,
@@ -213,27 +213,9 @@ async def test_get_basic_info_parsing(basic_info, result) -> None:
     assert info["door_open"] == result[12]
     assert info["door_open"] == result[12]
 
 
 
 
-@pytest.mark.asyncio
-@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
-async def test_verify_encryption_key(mock_parent_verify):
+def test_default_model_classvar():
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
-    key_id = "ff"
-    encryption_key = "ffffffffffffffffffffffffffffffff"
-
-    mock_parent_verify.return_value = True
-
-    result = await SwitchbotSmartThermostatRadiator.verify_encryption_key(
-        device=ble_device,
-        key_id=key_id,
-        encryption_key=encryption_key,
-        model=SMART_THERMOSTAT_RADIATOR_INFO.modelName,
-    )
-
-    mock_parent_verify.assert_awaited_once_with(
-        ble_device,
-        key_id,
-        encryption_key,
-        SMART_THERMOSTAT_RADIATOR_INFO.modelName,
+    device = SwitchbotSmartThermostatRadiator(
+        ble_device, "ff", "ffffffffffffffffffffffffffffffff"
     )
     )
-
-    assert result is True
+    assert device._model == SMART_THERMOSTAT_RADIATOR_INFO.modelName

+ 12 - 27
tests/test_strip_light.py

@@ -1,4 +1,4 @@
-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import AsyncMock, MagicMock
 
 
 import pytest
 import pytest
 from bleak.backends.device import BLEDevice
 from bleak.backends.device import BLEDevice
@@ -7,7 +7,7 @@ from switchbot import SwitchBotAdvertisement, SwitchbotModel
 from switchbot.const.light import ColorMode
 from switchbot.const.light import ColorMode
 from switchbot.devices import light_strip
 from switchbot.devices import light_strip
 from switchbot.devices.base_light import SwitchbotBaseLight
 from switchbot.devices.base_light import SwitchbotBaseLight
-from switchbot.devices.device import SwitchbotEncryptedDevice, SwitchbotOperationError
+from switchbot.devices.device import SwitchbotOperationError
 
 
 from . import (
 from . import (
     FLOOR_LAMP_INFO,
     FLOOR_LAMP_INFO,
@@ -304,32 +304,17 @@ async def test_set_effect_normalizes_case(device_case):
         assert device.get_effect() == test_effect  # Stored as provided
         assert device.get_effect() == test_effect  # Stored as provided
 
 
 
 
-@pytest.mark.asyncio
-@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
-async def test_verify_encryption_key(mock_parent_verify, device_case):
+@pytest.mark.parametrize(
+    ("dev_cls", "expected_model"),
+    [
+        (light_strip.SwitchbotStripLight3, SwitchbotModel.STRIP_LIGHT_3),
+        (light_strip.SwitchbotRgbicLight, SwitchbotModel.RGBICWW_STRIP_LIGHT),
+    ],
+)
+def test_default_model_classvar(dev_cls, expected_model):
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
-    key_id = "ff"
-    encryption_key = "ffffffffffffffffffffffffffffffff"
-
-    mock_parent_verify.return_value = True
-
-    adv_info, dev_cls = device_case
-
-    result = await dev_cls.verify_encryption_key(
-        device=ble_device,
-        key_id=key_id,
-        encryption_key=encryption_key,
-        model=adv_info.modelName,
-    )
-
-    mock_parent_verify.assert_awaited_once_with(
-        ble_device,
-        key_id,
-        encryption_key,
-        adv_info.modelName,
-    )
-
-    assert result is True
+    device = dev_cls(ble_device, "ff", "ffffffffffffffffffffffffffffffff")
+    assert device._model == expected_model
 
 
 
 
 def create_strip_light_device(init_data: dict | None = None):
 def create_strip_light_device(init_data: dict | None = None):