| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from bleak.backends.device import BLEDevice
- from switchbot import (
- SwitchBotAdvertisement,
- SwitchbotEncryptedDevice,
- SwitchbotModel,
- SwitchbotOperationError,
- )
- from switchbot.const.air_purifier import AirPurifierMode
- from switchbot.devices import air_purifier
- from .test_adv_parser import generate_ble_device
- common_params = [
- (b"7\x00\x00\x95-\x00", "7", SwitchbotModel.AIR_PURIFIER_TABLE_US),
- (b"*\x00\x00\x15\x04\x00", "*", SwitchbotModel.AIR_PURIFIER_US),
- (b"+\x00\x00\x15\x04\x00", "+", SwitchbotModel.AIR_PURIFIER_JP),
- (b"8\x00\x00\x95-\x00", "8", SwitchbotModel.AIR_PURIFIER_TABLE_JP),
- ]
- def create_device_for_command_testing(
- rawAdvData: bytes,
- model: str,
- model_name: SwitchbotModel,
- init_data: dict | None = None,
- ):
- ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
- device = air_purifier.SwitchbotAirPurifier(
- ble_device,
- "ff",
- "ffffffffffffffffffffffffffffffff",
- model=model_name,
- )
- device.update = AsyncMock()
- device.update_from_advertisement(
- make_advertisement_data(ble_device, rawAdvData, model, model_name, init_data)
- )
- device._send_command = AsyncMock()
- device._check_command_result = MagicMock()
- return device
- def make_advertisement_data(
- ble_device: BLEDevice,
- rawAdvData: bytes,
- model: str,
- model_name: SwitchbotModel,
- 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": rawAdvData,
- "data": {
- "isOn": True,
- "mode": "level_3",
- "isAqiValid": False,
- "child_lock": False,
- "speed": 100,
- "aqi_level": "excellent",
- "filter element working time": 405,
- "err_code": 0,
- "sequence_number": 161,
- }
- | init_data,
- "isEncrypted": False,
- "model": model,
- "modelFriendlyName": "Air Purifier",
- "modelName": model_name,
- },
- device=ble_device,
- rssi=-80,
- active=True,
- )
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("rawAdvData", "model", "model_name"),
- common_params,
- )
- @pytest.mark.parametrize(
- "pm25",
- [150],
- )
- async def test_status_from_process_adv(rawAdvData, model, model_name, pm25):
- device = create_device_for_command_testing(
- rawAdvData, model, model_name, {"pm25": pm25}
- )
- assert device.get_current_percentage() == 100
- assert device.is_on() is True
- assert device.get_current_aqi_level() == "excellent"
- assert device.get_current_mode() == "level_3"
- assert device.get_current_pm25() == 150
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("rawAdvData", "model", "model_name"),
- common_params,
- )
- async def test_get_basic_info_returns_none_when_no_data(rawAdvData, model, model_name):
- device = create_device_for_command_testing(rawAdvData, model, model_name)
- device._get_basic_info_by_multi_commands = AsyncMock(return_value=None)
- assert await device.get_basic_info() is None
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("rawAdvData", "model", "model_name"),
- common_params,
- )
- @pytest.mark.parametrize(
- "mode", ["level_1", "level_2", "level_3", "auto", "pet", "sleep"]
- )
- async def test_set_preset_mode(rawAdvData, model, model_name, mode):
- device = create_device_for_command_testing(
- rawAdvData, model, model_name, {"mode": mode}
- )
- await device.set_preset_mode(mode)
- assert device.get_current_mode() == mode
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("rawAdvData", "model", "model_name"),
- common_params,
- )
- async def test_turn_on(rawAdvData, model, model_name):
- device = create_device_for_command_testing(
- rawAdvData, model, model_name, {"isOn": True}
- )
- await device.turn_on()
- assert device.is_on() is True
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("rawAdvData", "model", "model_name"),
- common_params,
- )
- async def test_turn_off(rawAdvData, model, model_name):
- device = create_device_for_command_testing(
- rawAdvData, model, model_name, {"isOn": False}
- )
- await device.turn_off()
- assert device.is_on() is False
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("rawAdvData", "model", "model_name"),
- common_params,
- )
- @pytest.mark.parametrize(
- ("response", "expected"),
- [
- (b"\x00", None),
- (b"\x07", None),
- (b"\x01\x02\x03", b"\x01\x02\x03"),
- ],
- )
- async def test__get_basic_info(rawAdvData, model, model_name, response, expected):
- device = create_device_for_command_testing(rawAdvData, model, model_name)
- device._send_command = AsyncMock(return_value=response)
- result = await device._get_basic_info()
- assert result == expected
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "device_case",
- common_params,
- )
- @pytest.mark.parametrize(
- "info_case",
- [
- (
- bytearray(
- b"\x01\xa7\xe9\x8c\x08\x00\xb2\x01\x96\x00\x00\x00\x00\x01\x00\x17"
- ),
- bytearray(b"\x01\x01\x11\x22\x33\x44"),
- bytearray(b"\x01\x03"),
- [
- True,
- 2,
- "level_2",
- True,
- False,
- "excellent",
- 50,
- 1,
- 2.3,
- 0x44,
- True,
- ],
- ),
- (
- bytearray(
- b"\x01\xa8\xec\x8c\x08\x00\xb2\x01\x96\x00\x00\x00\x01\x00\x00\x17"
- ),
- bytearray(b"\x01\x01\xaa\xbb\xcc\x1e"),
- bytearray(b"\x01\x00"),
- [
- True,
- 2,
- "pet",
- True,
- False,
- "excellent",
- 50,
- 256,
- 2.3,
- 0x1E,
- False,
- ],
- ),
- ],
- )
- async def test_get_basic_info(device_case, info_case):
- rawAdvData, model, model_name = device_case
- basic_info, led_settings, led_status, result = info_case
- device = create_device_for_command_testing(rawAdvData, model, model_name)
- device._get_basic_info_by_multi_commands = AsyncMock(
- return_value=[basic_info, led_settings, led_status]
- )
- info = await device.get_basic_info()
- assert info["isOn"] == result[0]
- assert info["version_info"] == result[1]
- assert info["mode"] == result[2]
- assert info["isAqiValid"] == result[3]
- assert info["child_lock"] == result[4]
- assert info["aqi_level"] == result[5]
- assert info["speed"] == result[6]
- if model_name not in (
- SwitchbotModel.AIR_PURIFIER_JP,
- SwitchbotModel.AIR_PURIFIER_TABLE_JP,
- ):
- assert info["pm25"] == result[7]
- else:
- assert "pm25" not in info
- assert info["firmware"] == result[8]
- assert info["brightness"] == result[9]
- 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):
- 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,
- )
- assert result is True
- def test_get_modes():
- assert AirPurifierMode.get_modes() == [
- "level_1",
- "level_2",
- "level_3",
- "auto",
- "sleep",
- "pet",
- ]
- @pytest.mark.asyncio
- async def test_air_purifier_color_and_led_properties():
- raw_adv, model, model_name = common_params[0]
- device = create_device_for_command_testing(
- raw_adv,
- model,
- model_name,
- )
- assert device.color_modes == {air_purifier.ColorMode.RGB}
- assert device.color_mode == air_purifier.ColorMode.RGB
- @pytest.mark.asyncio
- async def test_set_percentage_validation_and_command():
- raw_adv, model, model_name = common_params[0]
- device = create_device_for_command_testing(
- raw_adv,
- model,
- model_name,
- {"mode": "level_2"},
- )
- device._check_command_result = MagicMock(return_value=True)
- device._send_command = AsyncMock(return_value=b"\x01")
- assert await device.set_percentage(25) is True
- device._send_command.assert_called_with(
- air_purifier.COMMAND_SET_PERCENTAGE.format(percentage=25)
- )
- with pytest.raises(ValueError, match="Percentage must be between 0 and 100"):
- await device.set_percentage(-1)
- with pytest.raises(ValueError, match="Percentage must be between 0 and 100"):
- await device.set_percentage(101)
- invalid_mode_device = create_device_for_command_testing(
- raw_adv,
- model,
- model_name,
- {"mode": "auto"},
- )
- with pytest.raises(ValueError, match="Percentage can only be set in LEVEL modes"):
- await invalid_mode_device.set_percentage(10)
- @pytest.mark.asyncio
- async def test_set_brightness_validation_and_command():
- raw_adv, model, model_name = common_params[0]
- device = create_device_for_command_testing(raw_adv, model, model_name)
- device._state = {"r": 1, "g": 2, "b": 3}
- device._check_command_result = MagicMock(return_value=True)
- device._send_command = AsyncMock(return_value=b"\x01")
- assert await device.set_brightness(10) is True
- device._send_command.assert_called_with(
- device._set_brightness_command.format("0102030A")
- )
- with pytest.raises(ValueError, match="Brightness must be between 0 and 100"):
- await device.set_brightness(101)
- @pytest.mark.asyncio
- async def test_set_rgb_validation_and_command():
- raw_adv, model, model_name = common_params[0]
- device = create_device_for_command_testing(raw_adv, model, model_name)
- device._check_command_result = MagicMock(return_value=True)
- device._send_command = AsyncMock(return_value=b"\x01")
- assert await device.set_rgb(20, 1, 2, 3) is True
- device._send_command.assert_called_with(device._set_rgb_command.format("01020314"))
- with pytest.raises(ValueError, match="Brightness must be between 0 and 100"):
- await device.set_rgb(101, 1, 2, 3)
- with pytest.raises(ValueError, match="r must be between 0 and 255"):
- await device.set_rgb(10, 256, 2, 3)
- with pytest.raises(ValueError, match="g must be between 0 and 255"):
- await device.set_rgb(10, 1, 256, 3)
- with pytest.raises(ValueError, match="b must be between 0 and 255"):
- await device.set_rgb(10, 1, 2, 256)
- @pytest.mark.asyncio
- async def test_led_and_light_sensitive_commands():
- raw_adv, model, model_name = common_params[0]
- device = create_device_for_command_testing(
- raw_adv, model, model_name, {"led_status": True}
- )
- device._check_command_result = MagicMock(return_value=True)
- device._send_command = AsyncMock(return_value=b"\x01")
- assert await device.turn_led_on() is True
- device._send_command.assert_called_with(device._turn_led_on_command)
- assert await device.turn_led_off() is True
- device._send_command.assert_called_with(device._turn_led_off_command)
- assert await device.open_light_sensitive_switch() is True
- device._send_command.assert_called_with(device._open_light_sensitive_switch_command)
- assert await device.close_light_sensitive_switch() is True
- device._send_command.assert_called_with(device._turn_led_on_command)
- device_off = create_device_for_command_testing(
- raw_adv,
- model,
- model_name,
- )
- device_off._check_command_result = MagicMock(return_value=True)
- device_off._send_command = AsyncMock(return_value=b"\x01")
- assert await device_off.close_light_sensitive_switch() is True
- device_off._send_command.assert_called_with(device_off._turn_led_on_command)
- @pytest.mark.asyncio
- async def test_air_purifier_cache_getters():
- raw_adv, model, model_name = common_params[0]
- device = create_device_for_command_testing(
- raw_adv,
- model,
- model_name,
- {
- "child_lock": True,
- "wireless_charging": True,
- "light_sensitive": True,
- "speed": 88,
- },
- )
- assert device.get_current_percentage() == 88
- assert device.is_child_lock_on() is True
- assert device.is_wireless_charging_on() is True
- assert device.is_light_sensitive_on() is True
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "operation_case",
- [
- ("open_child_lock", "_open_child_lock_command"),
- ("close_child_lock", "_close_child_lock_command"),
- ],
- )
- async def test_child_lock_operations(operation_case):
- """Child lock commands should always be forwarded correctly."""
- raw_adv, model, model_name = common_params[0]
- device = create_device_for_command_testing(raw_adv, model, model_name)
- operation_name, command_attr = operation_case
- command = getattr(device, command_attr)
- device._check_function_support = MagicMock()
- device._check_command_result = MagicMock(return_value=True)
- device._send_command = AsyncMock(return_value=b"\x01")
- operation = getattr(device, operation_name)
- assert await operation() is True
- device._check_function_support.assert_called_with(command)
- device._send_command.assert_called_with(command)
- device._check_command_result.assert_called_with(b"\x01", 0, {1})
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("raw_adv", "model", "model_name", "supported"),
- [
- (*common_params[0], True),
- (*common_params[3], True),
- (*common_params[1], False),
- (*common_params[2], False),
- ],
- )
- async def test_wireless_charging_model_support(raw_adv, model, model_name, supported):
- """Wireless charging operations should only succeed for table variants."""
- device = create_device_for_command_testing(raw_adv, model, model_name)
- if supported:
- device._check_command_result = MagicMock(return_value=True)
- device._send_command = AsyncMock(return_value=b"\x01")
- assert await device.open_wireless_charging() is True
- assert await device.close_wireless_charging() is True
- assert device._send_command.call_args_list == [
- ((device._open_wireless_charging_command,),),
- ((device._close_wireless_charging_command,),),
- ]
- else:
- with pytest.raises(SwitchbotOperationError):
- await device.open_wireless_charging()
- with pytest.raises(SwitchbotOperationError):
- await device.close_wireless_charging()
|