from unittest.mock import AsyncMock, Mock import pytest from bleak.backends.device import BLEDevice from switchbot import SwitchBotAdvertisement, SwitchbotModel from switchbot.devices import curtain from switchbot.devices.base_cover import COVER_EXT_SUM_KEY from .test_adv_parser import generate_ble_device def create_device_for_command_testing(calibration=True, reverse_mode=False): ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 50, calibration) ) curtain_device._send_multiple_commands = AsyncMock() curtain_device.update = AsyncMock() return curtain_device def make_advertisement_data( ble_device: BLEDevice, in_motion: bool, position: int, calibration: bool = True ): """Set advertisement data with defaults.""" return SwitchBotAdvertisement( address="aa:bb:cc:dd:ee:ff", data={ "rawAdvData": b"c\xc0X\x00\x11\x04", "data": { "calibration": calibration, "battery": 88, "inMotion": in_motion, "position": position, "lightLevel": 1, "deviceChain": 1, }, "isEncrypted": False, "model": "c", "modelFriendlyName": "Curtain", "modelName": SwitchbotModel.CURTAIN, }, device=ble_device, rssi=-80, active=True, ) @pytest.mark.parametrize("reverse_mode", [(True), (False)]) def test_device_passive_not_in_motion(reverse_mode): """Test passive not in motion advertisement.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, False, 0) ) assert curtain_device.is_opening() is False assert curtain_device.is_closing() is False @pytest.mark.parametrize("reverse_mode", [(True), (False)]) def test_device_passive_opening(reverse_mode): """Test passive opening advertisement.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 0) ) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 10) ) assert curtain_device.is_opening() is True assert curtain_device.is_closing() is False @pytest.mark.parametrize("reverse_mode", [(True), (False)]) def test_device_passive_closing(reverse_mode): """Test passive closing advertisement.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 100) ) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 90) ) assert curtain_device.is_opening() is False assert curtain_device.is_closing() is True @pytest.mark.parametrize("reverse_mode", [(True), (False)]) def test_device_passive_opening_then_stop(reverse_mode): """Test passive stopped after opening advertisement.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 0) ) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 10) ) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, False, 10) ) assert curtain_device.is_opening() is False assert curtain_device.is_closing() is False @pytest.mark.parametrize("reverse_mode", [(True), (False)]) def test_device_passive_closing_then_stop(reverse_mode): """Test passive stopped after closing advertisement.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 100) ) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 90) ) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, False, 90) ) assert curtain_device.is_opening() is False assert curtain_device.is_closing() is False @pytest.mark.asyncio @pytest.mark.parametrize("reverse_mode", [(True), (False)]) async def test_device_active_not_in_motion(reverse_mode): """Test active not in motion.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, False, 0) ) basic_info = bytes([0, 0, 0, 0, 0, 0, 100, 0]) async def custom_implementation(): return basic_info curtain_device._get_basic_info = Mock(side_effect=custom_implementation) await curtain_device.get_basic_info() assert curtain_device.is_opening() is False assert curtain_device.is_closing() is False @pytest.mark.asyncio @pytest.mark.parametrize("reverse_mode", [(True), (False)]) async def test_device_active_opening(reverse_mode): """Test active opening.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 0) ) basic_info = bytes([0, 0, 0, 0, 0, 67, 10, 0]) async def custom_implementation(): return basic_info curtain_device._get_basic_info = Mock(side_effect=custom_implementation) await curtain_device.get_basic_info() assert curtain_device.is_opening() is True assert curtain_device.is_closing() is False @pytest.mark.asyncio @pytest.mark.parametrize("reverse_mode", [(True), (False)]) async def test_device_active_closing(reverse_mode): """Test active closing.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 100) ) basic_info = bytes([0, 0, 0, 0, 0, 67, 90, 0]) async def custom_implementation(): return basic_info curtain_device._get_basic_info = Mock(side_effect=custom_implementation) await curtain_device.get_basic_info() assert curtain_device.is_opening() is False assert curtain_device.is_closing() is True @pytest.mark.asyncio @pytest.mark.parametrize("reverse_mode", [(True), (False)]) async def test_device_active_opening_then_stop(reverse_mode): """Test active stopped after opening.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 0) ) basic_info = bytes([0, 0, 0, 0, 0, 67, 10, 0]) async def custom_implementation(): return basic_info curtain_device._get_basic_info = Mock(side_effect=custom_implementation) await curtain_device.get_basic_info() basic_info = bytes([0, 0, 0, 0, 0, 0, 10, 0]) await curtain_device.get_basic_info() assert curtain_device.is_opening() is False assert curtain_device.is_closing() is False @pytest.mark.asyncio @pytest.mark.parametrize("reverse_mode", [(True), (False)]) async def test_device_active_closing_then_stop(reverse_mode): """Test active stopped after closing.""" ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 100) ) basic_info = bytes([0, 0, 0, 0, 0, 67, 90, 0]) async def custom_implementation(): return basic_info curtain_device._get_basic_info = Mock(side_effect=custom_implementation) await curtain_device.get_basic_info() basic_info = bytes([0, 0, 0, 0, 0, 0, 90, 0]) await curtain_device.get_basic_info() assert curtain_device.is_opening() is False assert curtain_device.is_closing() is False @pytest.mark.asyncio async def test_get_basic_info_returns_none_when_no_data(): ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 0) ) curtain_device._get_basic_info = AsyncMock(return_value=None) assert await curtain_device.get_basic_info() is None @pytest.mark.asyncio @pytest.mark.parametrize( "data,result", [ ( bytes([0, 1, 10, 2, 255, 255, 50, 4]), [1, 1, 2, "right_to_left", 1, 1, 50, 4], ), (bytes([0, 1, 10, 2, 0, 0, 50, 4]), [1, 1, 2, "left_to_right", 0, 0, 50, 4]), ], ) async def test_get_basic_info(data, result): ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") curtain_device = curtain.SwitchbotCurtain(ble_device) curtain_device.update_from_advertisement( make_advertisement_data(ble_device, True, 0) ) async def custom_implementation(): return data curtain_device._get_basic_info = Mock(side_effect=custom_implementation) info = await curtain_device.get_basic_info() assert info["battery"] == result[0] assert info["firmware"] == result[1] assert info["chainLength"] == result[2] assert info["openDirection"] == result[3] assert info["touchToOpen"] == result[4] assert info["light"] == result[4] assert info["fault"] == result[4] assert info["solarPanel"] == result[5] assert info["calibration"] == result[5] assert info["calibrated"] == result[5] assert info["inMotion"] == result[5] assert info["position"] == result[6] assert info["timers"] == result[7] @pytest.mark.asyncio async def test_open(): curtain_device = create_device_for_command_testing() await curtain_device.open() assert curtain_device.is_opening() is True assert curtain_device.is_closing() is False curtain_device._send_multiple_commands.assert_awaited_once() @pytest.mark.asyncio async def test_close(): curtain_device = create_device_for_command_testing() await curtain_device.close() assert curtain_device.is_opening() is False assert curtain_device.is_closing() is True curtain_device._send_multiple_commands.assert_awaited_once() @pytest.mark.asyncio async def test_stop(): curtain_device = create_device_for_command_testing() await curtain_device.stop() assert curtain_device.is_opening() is False assert curtain_device.is_closing() is False curtain_device._send_multiple_commands.assert_awaited_once() @pytest.mark.asyncio async def test_set_position_opening(): curtain_device = create_device_for_command_testing() await curtain_device.set_position(100) assert curtain_device.is_opening() is True assert curtain_device.is_closing() is False curtain_device._send_multiple_commands.assert_awaited_once() @pytest.mark.asyncio async def test_set_position_closing(): curtain_device = create_device_for_command_testing() await curtain_device.set_position(0) assert curtain_device.is_opening() is False assert curtain_device.is_closing() is True curtain_device._send_multiple_commands.assert_awaited_once() def test_get_position(): curtain_device = create_device_for_command_testing() assert curtain_device.get_position() == 50 @pytest.mark.asyncio async def test_get_extended_info_summary_sends_command(): curtain_device = create_device_for_command_testing() curtain_device._send_command = AsyncMock() await curtain_device.get_extended_info_summary() curtain_device._send_command.assert_awaited_once_with(key=COVER_EXT_SUM_KEY) @pytest.mark.asyncio @pytest.mark.parametrize("data_value", [(None), (b"\x07"), (b"\x00")]) async def test_get_extended_info_summary_returns_none_when_bad_data(data_value): curtain_device = create_device_for_command_testing() curtain_device._send_command = AsyncMock(return_value=data_value) assert await curtain_device.get_extended_info_summary() is None @pytest.mark.asyncio @pytest.mark.parametrize( "data,result", [ ([0, 0, 0], [True, False, False, "right_to_left"]), ([255, 255, 0], [False, True, True, "left_to_right"]), ], ) async def test_get_extended_info_summary_returns_device0(data, result): curtain_device = create_device_for_command_testing() curtain_device._send_command = AsyncMock(return_value=bytes(data)) ext_result = await curtain_device.get_extended_info_summary() assert ext_result["device0"]["openDirectionDefault"] == result[0] assert ext_result["device0"]["touchToOpen"] == result[1] assert ext_result["device0"]["light"] == result[2] assert ext_result["device0"]["openDirection"] == result[3] assert "device1" not in ext_result @pytest.mark.asyncio @pytest.mark.parametrize( "data,result", [ ([0, 0, 1], [True, False, False, "right_to_left"]), ([255, 255, 255], [False, True, True, "left_to_right"]), ], ) async def test_get_extended_info_summary_returns_device1(data, result): curtain_device = create_device_for_command_testing() curtain_device._send_command = AsyncMock(return_value=bytes(data)) ext_result = await curtain_device.get_extended_info_summary() assert ext_result["device1"]["openDirectionDefault"] == result[0] assert ext_result["device1"]["touchToOpen"] == result[1] assert ext_result["device1"]["light"] == result[2] assert ext_result["device1"]["openDirection"] == result[3] def test_get_light_level(): curtain_device = create_device_for_command_testing() assert curtain_device.get_light_level() == 1 @pytest.mark.parametrize("reverse_mode", [(True), (False)]) def test_is_reversed(reverse_mode): curtain_device = create_device_for_command_testing(reverse_mode=reverse_mode) assert curtain_device.is_reversed() == reverse_mode @pytest.mark.parametrize("calibration", [(True), (False)]) def test_is_calibrated(calibration): curtain_device = create_device_for_command_testing(calibration=calibration) assert curtain_device.is_calibrated() == calibration