diff --git a/switchbot/__init__.py b/switchbot/__init__.py index e418188..e021cd4 100644 --- a/switchbot/__init__.py +++ b/switchbot/__init__.py @@ -13,12 +13,15 @@ AirPurifierMode, BulbColorMode, CeilingLightColorMode, + ClimateAction, + ClimateMode, ColorMode, FanMode, HumidifierAction, HumidifierMode, HumidifierWaterLevel, LockStatus, + SmartThermostatRadiatorMode, StripLightColorMode, SwitchbotAccountConnectionError, SwitchbotApiError, @@ -54,6 +57,7 @@ SwitchbotRelaySwitch2PM, ) from .devices.roller_shade import SwitchbotRollerShade +from .devices.smart_thermostat_radiator import SwitchbotSmartThermostatRadiator from .devices.vacuum import SwitchbotVacuum from .discovery import GetSwitchbotDevices from .models import SwitchBotAdvertisement @@ -62,6 +66,8 @@ "AirPurifierMode", "BulbColorMode", "CeilingLightColorMode", + "ClimateAction", + "ClimateMode", "ColorMode", "FanMode", "GetSwitchbotDevices", @@ -69,6 +75,7 @@ "HumidifierMode", "HumidifierWaterLevel", "LockStatus", + "SmartThermostatRadiatorMode", "StripLightColorMode", "SwitchBotAdvertisement", "Switchbot", @@ -99,6 +106,7 @@ "SwitchbotRelaySwitch2PM", "SwitchbotRgbicLight", "SwitchbotRollerShade", + "SwitchbotSmartThermostatRadiator", "SwitchbotStripLight3", "SwitchbotSupportedType", "SwitchbotSupportedType", diff --git a/switchbot/adv_parser.py b/switchbot/adv_parser.py index 5964558..6cd6dd3 100644 --- a/switchbot/adv_parser.py +++ b/switchbot/adv_parser.py @@ -43,6 +43,7 @@ ) from .adv_parsers.remote import process_woremote from .adv_parsers.roller_shade import process_worollershade +from .adv_parsers.smart_thermostat_radiator import process_smart_thermostat_radiator from .adv_parsers.vacuum import process_vacuum, process_vacuum_k from .const import SwitchbotModel from .models import SwitchBotAdvertisement @@ -377,6 +378,12 @@ class SwitchbotSupportedType(TypedDict): "func": process_climate_panel, "manufacturer_id": 2409, }, + b"\x00\x116@": { + "modelName": SwitchbotModel.SMART_THERMOSTAT_RADIATOR, + "modelFriendlyName": "Smart Thermostat Radiator", + "func": process_smart_thermostat_radiator, + "manufacturer_id": 2409, + }, } _SWITCHBOT_MODEL_TO_CHAR = { diff --git a/switchbot/adv_parsers/smart_thermostat_radiator.py b/switchbot/adv_parsers/smart_thermostat_radiator.py new file mode 100644 index 0000000..1f6035f --- /dev/null +++ b/switchbot/adv_parsers/smart_thermostat_radiator.py @@ -0,0 +1,58 @@ +"""Smart Thermostat Radiator""" + +import logging + +from ..const.climate import SmartThermostatRadiatorMode + +_LOGGER = logging.getLogger(__name__) + + +def process_smart_thermostat_radiator( + data: bytes | None, mfr_data: bytes | None +) -> dict[str, bool | int | str]: + """Process Smart Thermostat Radiator data.""" + if mfr_data is None: + return {} + + _seq_num = mfr_data[6] + _isOn = bool(mfr_data[7] & 0b10000000) + _battery = mfr_data[7] & 0b01111111 + + temp_data = mfr_data[8:11] + target_decimal = (temp_data[0] >> 4) & 0x0F + local_decimal = temp_data[0] & 0x0F + + local_sign = 1 if (temp_data[1] & 0x80) else -1 + local_int = temp_data[1] & 0x7F + local_temp = local_sign * (local_int + (local_decimal / 10)) + + target_sign = 1 if (temp_data[2] & 0x80) else -1 + target_int = temp_data[2] & 0x7F + target_temp = target_sign * (target_int + (target_decimal / 10)) + + last_mode = SmartThermostatRadiatorMode.get_mode_name((mfr_data[11] >> 4) & 0x0F) + mode = SmartThermostatRadiatorMode.get_mode_name(mfr_data[11] & 0x07) + + need_update_temp = bool((mfr_data[12] >> 5) & 0x01) + restarted = bool((mfr_data[12] >> 4) & 0x01) + fault_code = (mfr_data[12] >> 1) & 0x07 + door_open = bool(mfr_data[12] & 0x01) + + result = { + "sequence_number": _seq_num, + "isOn": _isOn, + "battery": _battery, + "temperature": local_temp, + "target_temperature": target_temp, + "mode": mode, + "last_mode": last_mode, + "need_update_temp": need_update_temp, + "restarted": restarted, + "fault_code": fault_code, + "door_open": door_open, + } + + _LOGGER.debug( + "Smart Thermostat Radiator mfr data: %s, result: %s", mfr_data.hex(), result + ) + return result diff --git a/switchbot/const/__init__.py b/switchbot/const/__init__.py index 6b2191a..0b8ecaf 100644 --- a/switchbot/const/__init__.py +++ b/switchbot/const/__init__.py @@ -4,6 +4,7 @@ from ..enum import StrEnum from .air_purifier import AirPurifierMode +from .climate import ClimateAction, ClimateMode, SmartThermostatRadiatorMode from .evaporative_humidifier import ( HumidifierAction, HumidifierMode, @@ -98,6 +99,7 @@ class SwitchbotModel(StrEnum): RGBICWW_FLOOR_LAMP = "RGBICWW Floor Lamp" K11_VACUUM = "K11+ Vacuum" CLIMATE_PANEL = "Climate Panel" + SMART_THERMOSTAT_RADIATOR = "Smart Thermostat Radiator" __all__ = [ @@ -107,12 +109,15 @@ class SwitchbotModel(StrEnum): "AirPurifierMode", "BulbColorMode", "CeilingLightColorMode", + "ClimateAction", + "ClimateMode", "ColorMode", "FanMode", "HumidifierAction", "HumidifierMode", "HumidifierWaterLevel", "LockStatus", + "SmartThermostatRadiatorMode", "StripLightColorMode", "SwitchbotAccountConnectionError", "SwitchbotApiError", diff --git a/switchbot/const/climate.py b/switchbot/const/climate.py new file mode 100644 index 0000000..a3f4d38 --- /dev/null +++ b/switchbot/const/climate.py @@ -0,0 +1,50 @@ +"""Representation of climate-related constants.""" + +from enum import Enum + + +class ClimateMode(Enum): + """Climate Modes.""" + + OFF = 0 + HEAT = 1 + COOL = 2 + HEAT_COOL = 3 + AUTO = 4 + DRY = 5 + FAN_ONLY = 6 + + +class ClimateAction(Enum): + """Climate Actions.""" + + OFF = 0 + IDLE = 1 + HEATING = 2 + + +class SmartThermostatRadiatorMode(Enum): + """Smart Thermostat Radiator Modes.""" + + SCHEDULE = 0 + MANUAL = 1 + OFF = 2 + ECONOMIC = 3 + COMFORT = 4 + FAST_HEATING = 5 + + @property + def lname(self) -> str: + return self.name.lower() + + @classmethod + def get_modes(cls) -> list[str]: + return [mode.lname for mode in cls] + + @classmethod + def get_mode_name(cls, mode_value: int) -> str: + return cls(mode_value).lname + + @classmethod + def get_valid_modes(cls) -> list[str]: + return [mode.lname for mode in cls if mode != cls.OFF] diff --git a/switchbot/devices/smart_thermostat_radiator.py b/switchbot/devices/smart_thermostat_radiator.py new file mode 100644 index 0000000..e0a9798 --- /dev/null +++ b/switchbot/devices/smart_thermostat_radiator.py @@ -0,0 +1,223 @@ +"""Smart Thermostat Radiator Device.""" + +import logging +from typing import Any + +from bleak.backends.device import BLEDevice + +from ..const import SwitchbotModel +from ..const.climate import ClimateAction, ClimateMode +from ..const.climate import SmartThermostatRadiatorMode as STRMode +from .device import ( + SwitchbotEncryptedDevice, + SwitchbotOperationError, + SwitchbotSequenceDevice, + update_after_operation, +) + +_LOGGER = logging.getLogger(__name__) + +DEVICE_GET_BASIC_SETTINGS_KEY = "5702" + +_modes = STRMode.get_valid_modes() +SMART_THERMOSTAT_TO_HA_HVAC_MODE = { + "off": ClimateMode.OFF, + **dict.fromkeys(_modes, ClimateMode.HEAT), +} + +COMMAND_SET_MODE = { + mode.lname: f"570F7800{index:02X}" for index, mode in enumerate(STRMode) +} + +# fast heating default use max temperature +COMMAND_SET_TEMP = { + STRMode.MANUAL.lname: "570F7801{temp:04X}", + STRMode.ECONOMIC.lname: "570F7802{temp:02X}", + STRMode.COMFORT.lname: "570F7803{temp:02X}", + STRMode.SCHEDULE.lname: "570F7806{temp:04X}", +} + +MODE_TEMP_RANGE = { + STRMode.ECONOMIC.lname: (10.0, 20.0), + STRMode.COMFORT.lname: (10.0, 25.0), +} + +DEFAULT_TEMP_RANGE = (5.0, 35.0) + + +class SwitchbotSmartThermostatRadiator( + SwitchbotSequenceDevice, SwitchbotEncryptedDevice +): + """Representation of a Switchbot Smart Thermostat Radiator.""" + + _turn_off_command = "570100" + _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 + def min_temperature(self) -> float: + """Return the minimum target temperature.""" + return MODE_TEMP_RANGE.get(self.preset_mode, DEFAULT_TEMP_RANGE)[0] + + @property + def max_temperature(self) -> float: + """Return the maximum target temperature.""" + return MODE_TEMP_RANGE.get(self.preset_mode, DEFAULT_TEMP_RANGE)[1] + + @property + def preset_modes(self) -> list[str]: + """Return the supported preset modes.""" + return STRMode.get_modes() + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self.get_current_mode() + + @property + def hvac_modes(self) -> set[ClimateMode]: + """Return the supported hvac modes.""" + return {ClimateMode.HEAT, ClimateMode.OFF} + + @property + def hvac_mode(self) -> ClimateMode | None: + """Return the current hvac mode.""" + return SMART_THERMOSTAT_TO_HA_HVAC_MODE.get(self.preset_mode, ClimateMode.OFF) + + @property + def hvac_action(self) -> ClimateAction | None: + """Return current action.""" + return self.get_action() + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.get_current_temperature() + + @property + def target_temperature(self) -> float | None: + """Return the target temperature.""" + return self.get_target_temperature() + + @update_after_operation + async def set_hvac_mode(self, hvac_mode: ClimateMode) -> None: + """Set the hvac mode.""" + if hvac_mode == ClimateMode.OFF: + return await self.turn_off() + return await self.set_preset_mode("comfort") + + @update_after_operation + async def set_preset_mode(self, preset_mode: str) -> bool: + """Send command to set thermostat preset_mode.""" + return await self._send_command(COMMAND_SET_MODE[preset_mode]) + + @update_after_operation + async def set_target_temperature(self, temperature: float) -> bool: + """Send command to set target temperature.""" + if self.preset_mode == STRMode.OFF.lname: + raise SwitchbotOperationError("Cannot set temperature when mode is OFF.") + if self.preset_mode == STRMode.FAST_HEATING.lname: + raise SwitchbotOperationError( + "Fast Heating mode defaults to max temperature." + ) + + temp_value = int(temperature * 10) + cmd = COMMAND_SET_TEMP[self.preset_mode].format(temp=temp_value) + + _LOGGER.debug( + "Setting temperature %.1f°C in mode %s → cmd=%s", + temperature, + self.preset_mode, + cmd, + ) + return await self._send_command(cmd) + + async def get_basic_info(self) -> dict[str, Any] | None: + """Get device basic settings.""" + if not (_data := await self._get_basic_info()): + return None + _LOGGER.debug("data: %s", _data) + + battery = _data[1] + firmware = _data[2] / 10.0 + hardware = _data[3] + last_mode = STRMode.get_mode_name((_data[4] >> 3) & 0x07) + mode = STRMode.get_mode_name(_data[4] & 0x07) + temp_raw_value = _data[5] << 8 | _data[6] + temp_sign = 1 if temp_raw_value >> 15 else -1 + temperature = temp_sign * (temp_raw_value & 0x7FFF) / 10.0 + manual_target_temp = (_data[7] << 8 | _data[8]) / 10.0 + comfort_target_temp = _data[9] / 10.0 + economic_target_temp = _data[10] / 10.0 + fast_heat_time = _data[11] + child_lock = bool(_data[12] & 0x03) + target_temp = (_data[13] << 8 | _data[14]) / 10.0 + door_open = bool(_data[14] & 0x01) + + result = { + "battery": battery, + "firmware": firmware, + "hardware": hardware, + "last_mode": last_mode, + "mode": mode, + "temperature": temperature, + "manual_target_temp": manual_target_temp, + "comfort_target_temp": comfort_target_temp, + "economic_target_temp": economic_target_temp, + "fast_heat_time": fast_heat_time, + "child_lock": child_lock, + "target_temp": target_temp, + "door_open": door_open, + } + + _LOGGER.debug("Smart Thermostat Radiator basic info: %s", result) + return result + + def is_on(self) -> bool | None: + """Return true if the thermostat is on.""" + return self._get_adv_value("isOn") + + def get_current_mode(self) -> str | None: + """Return the current mode of the thermostat.""" + return self._get_adv_value("mode") + + def door_open(self) -> bool | None: + """Return true if the door is open.""" + return self._get_adv_value("door_open") + + def get_current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._get_adv_value("temperature") + + def get_target_temperature(self) -> float | None: + """Return the target temperature.""" + return self._get_adv_value("target_temperature") + + def get_action(self) -> ClimateAction: + """Return current action from cache.""" + if not self.is_on(): + return ClimateAction.OFF + return ClimateAction.HEATING diff --git a/tests/__init__.py b/tests/__init__.py index 85c9514..d7cf681 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -80,3 +80,25 @@ class AdvTestCase: "Rgbic Floor Lamp", SwitchbotModel.RGBICWW_FLOOR_LAMP, ) + + +SMART_THERMOSTAT_RADIATOR_INFO = AdvTestCase( + b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00", + b"\x00 d\x00\x116@", + { + "battery": 100, + "door_open": False, + "fault_code": 0, + "isOn": True, + "last_mode": "comfort", + "mode": "manual", + "sequence_number": 54, + "need_update_temp": False, + "restarted": False, + "target_temperature": 35.0, + "temperature": 28.0, + }, + b"\x00\x116@", + "Smart Thermostat Radiator", + SwitchbotModel.SMART_THERMOSTAT_RADIATOR, +) diff --git a/tests/test_adv_parser.py b/tests/test_adv_parser.py index ee6008c..f60d21c 100644 --- a/tests/test_adv_parser.py +++ b/tests/test_adv_parser.py @@ -3448,6 +3448,26 @@ def test_humidifer_with_empty_data() -> None: "Climate Panel", SwitchbotModel.CLIMATE_PANEL, ), + AdvTestCase( + b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00", + b"\x00 d\x00\x116@", + { + "battery": 100, + "door_open": False, + "fault_code": 0, + "isOn": True, + "last_mode": "comfort", + "mode": "manual", + "sequence_number": 54, + "need_update_temp": False, + "restarted": False, + "target_temperature": 35.0, + "temperature": 28.0, + }, + b"\x00\x116@", + "Smart Thermostat Radiator", + SwitchbotModel.SMART_THERMOSTAT_RADIATOR, + ), ], ) def test_adv_active(test_case: AdvTestCase) -> None: @@ -3680,6 +3700,26 @@ def test_adv_active(test_case: AdvTestCase) -> None: "Climate Panel", SwitchbotModel.CLIMATE_PANEL, ), + AdvTestCase( + b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00", + None, + { + "battery": 100, + "door_open": False, + "fault_code": 0, + "isOn": True, + "last_mode": "comfort", + "mode": "manual", + "sequence_number": 54, + "need_update_temp": False, + "restarted": False, + "target_temperature": 35.0, + "temperature": 28.0, + }, + b"\x00\x116@", + "Smart Thermostat Radiator", + SwitchbotModel.SMART_THERMOSTAT_RADIATOR, + ), ], ) def test_adv_passive(test_case: AdvTestCase) -> None: @@ -3853,6 +3893,14 @@ def test_adv_passive(test_case: AdvTestCase) -> None: "Climate Panel", SwitchbotModel.CLIMATE_PANEL, ), + AdvTestCase( + None, + b"\x00 d\x00\x116@", + {}, + b"\x00\x116@", + "Smart Thermostat Radiator", + SwitchbotModel.SMART_THERMOSTAT_RADIATOR, + ), ], ) def test_adv_with_empty_data(test_case: AdvTestCase) -> None: diff --git a/tests/test_smart_thermostat_radiator.py b/tests/test_smart_thermostat_radiator.py new file mode 100644 index 0000000..069cb8e --- /dev/null +++ b/tests/test_smart_thermostat_radiator.py @@ -0,0 +1,239 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from bleak.backends.device import BLEDevice + +from switchbot import SwitchBotAdvertisement +from switchbot.const.climate import ClimateAction, ClimateMode +from switchbot.const.climate import SmartThermostatRadiatorMode as STRMode +from switchbot.devices.device import SwitchbotEncryptedDevice, SwitchbotOperationError +from switchbot.devices.smart_thermostat_radiator import ( + COMMAND_SET_MODE, + COMMAND_SET_TEMP, + SwitchbotSmartThermostatRadiator, +) + +from . import SMART_THERMOSTAT_RADIATOR_INFO +from .test_adv_parser import AdvTestCase, generate_ble_device + + +def create_device_for_command_testing( + adv_info: AdvTestCase, + init_data: dict | None = None, +): + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + device = SwitchbotSmartThermostatRadiator( + ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=adv_info.modelName + ) + device.update_from_advertisement( + make_advertisement_data(ble_device, adv_info, init_data) + ) + device._send_command = AsyncMock() + device._check_command_result = MagicMock() + device.update = AsyncMock() + return device + + +def make_advertisement_data( + ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None +): + """Set advertisement data with defaults.""" + if init_data is None: + init_data = {} + + return SwitchBotAdvertisement( + address="aa:bb:cc:dd:ee:ff", + data={ + "rawAdvData": adv_info.service_data, + "data": adv_info.data | init_data, + "isEncrypted": False, + "model": adv_info.model, + "modelFriendlyName": adv_info.modelFriendlyName, + "modelName": adv_info.modelName, + } + | init_data, + device=ble_device, + rssi=-80, + active=True, + ) + + +@pytest.mark.asyncio +async def test_default_info() -> None: + device = create_device_for_command_testing(SMART_THERMOSTAT_RADIATOR_INFO) + + assert device.min_temperature == 5.0 + assert device.max_temperature == 35.0 + assert device.preset_mode == STRMode.MANUAL.lname + assert device.preset_modes == STRMode.get_modes() + assert device.hvac_mode == ClimateMode.HEAT + assert device.hvac_modes == {ClimateMode.OFF, ClimateMode.HEAT} + assert device.hvac_action == ClimateAction.HEATING + assert device.target_temperature == 35.0 + assert device.current_temperature == 28.0 + assert device.door_open() is False + + +@pytest.mark.asyncio +async def test_default_info_with_off_mode() -> None: + device = create_device_for_command_testing( + SMART_THERMOSTAT_RADIATOR_INFO, {"mode": STRMode.OFF.lname, "isOn": False} + ) + assert device.hvac_action == ClimateAction.OFF + + +@pytest.mark.parametrize( + ("mode", "expected_command"), + [ + (ClimateMode.OFF, "570100"), + (ClimateMode.HEAT, COMMAND_SET_MODE[STRMode.COMFORT.lname]), + ], +) +@pytest.mark.asyncio +async def test_set_hvac_mode_commands(mode, expected_command) -> None: + device = create_device_for_command_testing(SMART_THERMOSTAT_RADIATOR_INFO) + + await device.set_hvac_mode(mode) + device._send_command.assert_awaited_with(expected_command) + + +@pytest.mark.parametrize( + ("preset_mode", "expected_command"), + [ + (STRMode.SCHEDULE.lname, COMMAND_SET_MODE[STRMode.SCHEDULE.lname]), + (STRMode.MANUAL.lname, COMMAND_SET_MODE[STRMode.MANUAL.lname]), + (STRMode.OFF.lname, COMMAND_SET_MODE[STRMode.OFF.lname]), + (STRMode.ECONOMIC.lname, COMMAND_SET_MODE[STRMode.ECONOMIC.lname]), + (STRMode.COMFORT.lname, COMMAND_SET_MODE[STRMode.COMFORT.lname]), + (STRMode.FAST_HEATING.lname, COMMAND_SET_MODE[STRMode.FAST_HEATING.lname]), + ], +) +@pytest.mark.asyncio +async def test_set_preset_mode_commands(preset_mode, expected_command) -> None: + device = create_device_for_command_testing(SMART_THERMOSTAT_RADIATOR_INFO) + + await device.set_preset_mode(preset_mode) + device._send_command.assert_awaited_with(expected_command) + + +@pytest.mark.asyncio +async def test_set_target_temperature_command() -> None: + device = create_device_for_command_testing(SMART_THERMOSTAT_RADIATOR_INFO) + + await device.set_target_temperature(22.5) + device._send_command.assert_awaited_with( + COMMAND_SET_TEMP[STRMode.MANUAL.lname].format(temp=225) + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("mode", "match"), + [ + (STRMode.OFF.lname, "Cannot set temperature when mode is OFF."), + (STRMode.FAST_HEATING.lname, "Fast Heating mode defaults to max temperature."), + ], +) +async def test_set_target_temperature_with_invalid_mode(mode, match) -> None: + device = create_device_for_command_testing( + SMART_THERMOSTAT_RADIATOR_INFO, {"mode": mode} + ) + + with pytest.raises(SwitchbotOperationError, match=match): + await device.set_target_temperature(22.5) + + +@pytest.mark.asyncio +async def test_get_basic_info_none() -> None: + device = create_device_for_command_testing(SMART_THERMOSTAT_RADIATOR_INFO) + device._get_basic_info = AsyncMock(return_value=None) + + assert await device.get_basic_info() is None + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("basic_info", "result"), + [ + ( + b"\x01d\x08>\x14\x80\xe6\x00(\x82\xbe\x00T\x00\x82\x00\x00", + [ + 100, + 0.8, + 62, + "off", + "comfort", + 23.0, + 4.0, + 13.0, + 19.0, + 0, + False, + 13.0, + False, + ], + ), + ( + b"\x01d\x08>#\x80\xf0\x00(\x82\xbe\x00T\x00\x82\x00\x00", + [ + 100, + 0.8, + 62, + "comfort", + "economic", + 24.0, + 4.0, + 13.0, + 19.0, + 0, + False, + 13.0, + False, + ], + ), + ], +) +async def test_get_basic_info_parsing(basic_info, result) -> None: + device = create_device_for_command_testing(SMART_THERMOSTAT_RADIATOR_INFO) + device._get_basic_info = AsyncMock(return_value=basic_info) + + info = await device.get_basic_info() + assert info["battery"] == result[0] + assert info["firmware"] == result[1] + assert info["hardware"] == result[2] + assert info["last_mode"] == result[3] + assert info["mode"] == result[4] + assert info["temperature"] == result[5] + assert info["manual_target_temp"] == result[6] + assert info["comfort_target_temp"] == result[7] + assert info["economic_target_temp"] == result[8] + assert info["fast_heat_time"] == result[9] + assert info["child_lock"] == result[10] + assert info["target_temp"] == result[11] + 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): + 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, + ) + + assert result is True