PySwitchbot 0.68.0__tar.gz → 0.68.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PKG-INFO +1 -1
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/setup.py +1 -1
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/__init__.py +6 -1
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/device.py +22 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/lock.py +15 -8
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/relay_switch.py +23 -1
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_encrypted_device.py +19 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_lock.py +30 -22
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_relay_switch.py +44 -6
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/LICENSE +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/MANIFEST.in +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/SOURCES.txt +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/README.md +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/pyproject.toml +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/setup.cfg +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parser.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/__init__.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/light.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/fan.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/enum.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/models.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_adv_parser.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_air_purifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_bulb.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_ceiling_light.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_colormode_imports.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_fan.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_helpers.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_hub3.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_roller_shade.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_strip_light.py +0 -0
- {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_vacuum.py +0 -0
|
@@ -43,7 +43,11 @@ from .devices.humidifier import SwitchbotHumidifier
|
|
|
43
43
|
from .devices.light_strip import SwitchbotLightStrip, SwitchbotStripLight3
|
|
44
44
|
from .devices.lock import SwitchbotLock
|
|
45
45
|
from .devices.plug import SwitchbotPlugMini
|
|
46
|
-
from .devices.relay_switch import
|
|
46
|
+
from .devices.relay_switch import (
|
|
47
|
+
SwitchbotGarageDoorOpener,
|
|
48
|
+
SwitchbotRelaySwitch,
|
|
49
|
+
SwitchbotRelaySwitch2PM,
|
|
50
|
+
)
|
|
47
51
|
from .devices.roller_shade import SwitchbotRollerShade
|
|
48
52
|
from .devices.vacuum import SwitchbotVacuum
|
|
49
53
|
from .discovery import GetSwitchbotDevices
|
|
@@ -77,6 +81,7 @@ __all__ = [
|
|
|
77
81
|
"SwitchbotEncryptedDevice",
|
|
78
82
|
"SwitchbotEvaporativeHumidifier",
|
|
79
83
|
"SwitchbotFan",
|
|
84
|
+
"SwitchbotGarageDoorOpener",
|
|
80
85
|
"SwitchbotHumidifier",
|
|
81
86
|
"SwitchbotLightStrip",
|
|
82
87
|
"SwitchbotLock",
|
|
@@ -127,6 +127,8 @@ class SwitchbotBaseDevice:
|
|
|
127
127
|
|
|
128
128
|
_turn_on_command: str | None = None
|
|
129
129
|
_turn_off_command: str | None = None
|
|
130
|
+
_open_command: str | None = None
|
|
131
|
+
_close_command: str | None = None
|
|
130
132
|
_press_command: str | None = None
|
|
131
133
|
|
|
132
134
|
def __init__(
|
|
@@ -719,6 +721,20 @@ class SwitchbotBaseDevice:
|
|
|
719
721
|
result = await self._send_command(self._turn_off_command)
|
|
720
722
|
return self._check_command_result(result, 0, {1})
|
|
721
723
|
|
|
724
|
+
@update_after_operation
|
|
725
|
+
async def open(self) -> bool:
|
|
726
|
+
"""Open the device."""
|
|
727
|
+
self._check_function_support(self._open_command)
|
|
728
|
+
result = await self._send_command(self._open_command)
|
|
729
|
+
return self._check_command_result(result, 0, {1})
|
|
730
|
+
|
|
731
|
+
@update_after_operation
|
|
732
|
+
async def close(self) -> bool:
|
|
733
|
+
"""Close the device."""
|
|
734
|
+
self._check_function_support(self._close_command)
|
|
735
|
+
result = await self._send_command(self._close_command)
|
|
736
|
+
return self._check_command_result(result, 0, {1})
|
|
737
|
+
|
|
722
738
|
@update_after_operation
|
|
723
739
|
async def press(self) -> bool:
|
|
724
740
|
"""Press the device."""
|
|
@@ -955,6 +971,12 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
|
|
|
955
971
|
if len(data) == 0:
|
|
956
972
|
return b""
|
|
957
973
|
if self._iv is None:
|
|
974
|
+
if self._expected_disconnect:
|
|
975
|
+
_LOGGER.debug(
|
|
976
|
+
"%s: Cannot decrypt, IV is None during expected disconnect",
|
|
977
|
+
self.name,
|
|
978
|
+
)
|
|
979
|
+
return b""
|
|
958
980
|
raise RuntimeError("Cannot decrypt: IV is None")
|
|
959
981
|
decryptor = self._get_cipher().decryptor()
|
|
960
982
|
return decryptor.update(data) + decryptor.finalize()
|
|
@@ -37,7 +37,12 @@ COMMAND_LOCK = {
|
|
|
37
37
|
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000000",
|
|
38
38
|
SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e0101000000",
|
|
39
39
|
}
|
|
40
|
-
COMMAND_ENABLE_NOTIFICATIONS =
|
|
40
|
+
COMMAND_ENABLE_NOTIFICATIONS = {
|
|
41
|
+
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0e01001e00008101",
|
|
42
|
+
SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0e01001e00008101",
|
|
43
|
+
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0e01001e00008104",
|
|
44
|
+
SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0e01001e00008107",
|
|
45
|
+
}
|
|
41
46
|
COMMAND_DISABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e00"
|
|
42
47
|
|
|
43
48
|
MOVING_STATUSES = {LockStatus.LOCKING, LockStatus.UNLOCKING}
|
|
@@ -197,12 +202,8 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
197
202
|
return _data
|
|
198
203
|
|
|
199
204
|
async def _enable_notifications(self) -> bool:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
result = await self._send_command(COMMAND_ENABLE_NOTIFICATIONS)
|
|
203
|
-
if self._check_command_result(result, 0, COMMAND_RESULT_EXPECTED_VALUES):
|
|
204
|
-
self._notifications_enabled = True
|
|
205
|
-
return self._notifications_enabled
|
|
205
|
+
result = await self._send_command(COMMAND_ENABLE_NOTIFICATIONS[self._model])
|
|
206
|
+
return self._check_command_result(result, 0, COMMAND_RESULT_EXPECTED_VALUES)
|
|
206
207
|
|
|
207
208
|
async def _disable_notifications(self) -> bool:
|
|
208
209
|
if not self._notifications_enabled:
|
|
@@ -213,7 +214,13 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
213
214
|
return not self._notifications_enabled
|
|
214
215
|
|
|
215
216
|
def _notification_handler(self, _sender: int, data: bytearray) -> None:
|
|
216
|
-
if self.
|
|
217
|
+
if self._check_command_result(data, 0, {0xF}):
|
|
218
|
+
if self._expected_disconnect:
|
|
219
|
+
_LOGGER.debug(
|
|
220
|
+
"%s: Ignoring lock notification during expected disconnect",
|
|
221
|
+
self.name,
|
|
222
|
+
)
|
|
223
|
+
return
|
|
217
224
|
self._update_lock_status(data)
|
|
218
225
|
else:
|
|
219
226
|
super()._notification_handler(_sender, data)
|
|
@@ -61,7 +61,6 @@ class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
61
61
|
|
|
62
62
|
_turn_on_command = f"{COMMAND_CONTROL}010100"
|
|
63
63
|
_turn_off_command = f"{COMMAND_CONTROL}010000"
|
|
64
|
-
_press_command = f"{COMMAND_CONTROL}110329" # for garage door opener toggle
|
|
65
64
|
|
|
66
65
|
def __init__(
|
|
67
66
|
self,
|
|
@@ -205,6 +204,29 @@ class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
205
204
|
"""Return switch state from cache."""
|
|
206
205
|
return self._get_adv_value("isOn")
|
|
207
206
|
|
|
207
|
+
def door_open(self) -> bool | None:
|
|
208
|
+
"""Return garage door state from cache."""
|
|
209
|
+
return self._get_adv_value("door_open")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class SwitchbotGarageDoorOpener(SwitchbotRelaySwitch):
|
|
213
|
+
"""Representation of a Switchbot garage door opener."""
|
|
214
|
+
|
|
215
|
+
_open_command = f"{COMMAND_CONTROL}110129"
|
|
216
|
+
_close_command = f"{COMMAND_CONTROL}110229"
|
|
217
|
+
_press_command = f"{COMMAND_CONTROL}110329" # for garage door opener toggle
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
device: BLEDevice,
|
|
222
|
+
key_id: str,
|
|
223
|
+
encryption_key: str,
|
|
224
|
+
interface: int = 0,
|
|
225
|
+
model: SwitchbotModel = SwitchbotModel.GARAGE_DOOR_OPENER,
|
|
226
|
+
**kwargs: Any,
|
|
227
|
+
) -> None:
|
|
228
|
+
super().__init__(device, key_id, encryption_key, interface, model, **kwargs)
|
|
229
|
+
|
|
208
230
|
|
|
209
231
|
class SwitchbotRelaySwitch2PM(SwitchbotRelaySwitch):
|
|
210
232
|
"""Representation of a Switchbot relay switch 2pm."""
|
|
@@ -365,3 +365,22 @@ async def test_empty_data_encryption_decryption() -> None:
|
|
|
365
365
|
# Test empty decryption
|
|
366
366
|
decrypted = device._decrypt(bytearray())
|
|
367
367
|
assert decrypted == b""
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@pytest.mark.asyncio
|
|
371
|
+
async def test_decrypt_with_none_iv_during_disconnect() -> None:
|
|
372
|
+
"""Test that decryption returns empty bytes when IV is None during expected disconnect."""
|
|
373
|
+
device = create_encrypted_device()
|
|
374
|
+
|
|
375
|
+
# Simulate disconnection in progress
|
|
376
|
+
device._expected_disconnect = True
|
|
377
|
+
device._iv = None
|
|
378
|
+
|
|
379
|
+
# Should return empty bytes instead of raising
|
|
380
|
+
result = device._decrypt(bytearray(b"encrypted_data"))
|
|
381
|
+
assert result == b""
|
|
382
|
+
|
|
383
|
+
# Verify it still raises when not disconnecting
|
|
384
|
+
device._expected_disconnect = False
|
|
385
|
+
with pytest.raises(RuntimeError, match="Cannot decrypt: IV is None"):
|
|
386
|
+
device._decrypt(bytearray(b"encrypted_data"))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from unittest.mock import AsyncMock, Mock, patch
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -371,27 +372,6 @@ async def test_enable_notifications(model: str):
|
|
|
371
372
|
with patch.object(device, "_send_command", return_value=b"\x01\x00"):
|
|
372
373
|
result = await device._enable_notifications()
|
|
373
374
|
assert result is True
|
|
374
|
-
assert device._notifications_enabled is True
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
@pytest.mark.asyncio
|
|
378
|
-
@pytest.mark.parametrize(
|
|
379
|
-
"model",
|
|
380
|
-
[
|
|
381
|
-
SwitchbotModel.LOCK,
|
|
382
|
-
SwitchbotModel.LOCK_LITE,
|
|
383
|
-
SwitchbotModel.LOCK_PRO,
|
|
384
|
-
SwitchbotModel.LOCK_ULTRA,
|
|
385
|
-
],
|
|
386
|
-
)
|
|
387
|
-
async def test_enable_notifications_already_enabled(model: str):
|
|
388
|
-
"""Test _enable_notifications when already enabled."""
|
|
389
|
-
device = create_device_for_command_testing(model)
|
|
390
|
-
device._notifications_enabled = True
|
|
391
|
-
with patch.object(device, "_send_command") as mock_send:
|
|
392
|
-
result = await device._enable_notifications()
|
|
393
|
-
assert result is True
|
|
394
|
-
mock_send.assert_not_called()
|
|
395
375
|
|
|
396
376
|
|
|
397
377
|
@pytest.mark.asyncio
|
|
@@ -466,7 +446,7 @@ def test_notification_handler_not_enabled(model: str):
|
|
|
466
446
|
"""Test _notification_handler when notifications not enabled."""
|
|
467
447
|
device = create_device_for_command_testing(model)
|
|
468
448
|
device._notifications_enabled = False
|
|
469
|
-
data = bytearray(b"\
|
|
449
|
+
data = bytearray(b"\x01\x00\x00\x00\x80\x00\x00\x00\x00\x00")
|
|
470
450
|
with (
|
|
471
451
|
patch.object(device, "_update_lock_status") as mock_update,
|
|
472
452
|
patch.object(
|
|
@@ -478,6 +458,34 @@ def test_notification_handler_not_enabled(model: str):
|
|
|
478
458
|
mock_super.assert_called_once()
|
|
479
459
|
|
|
480
460
|
|
|
461
|
+
@pytest.mark.parametrize(
|
|
462
|
+
"model",
|
|
463
|
+
[
|
|
464
|
+
SwitchbotModel.LOCK,
|
|
465
|
+
SwitchbotModel.LOCK_LITE,
|
|
466
|
+
SwitchbotModel.LOCK_PRO,
|
|
467
|
+
SwitchbotModel.LOCK_ULTRA,
|
|
468
|
+
],
|
|
469
|
+
)
|
|
470
|
+
def test_notification_handler_during_disconnect(
|
|
471
|
+
model: str, caplog: pytest.LogCaptureFixture
|
|
472
|
+
) -> None:
|
|
473
|
+
"""Test _notification_handler during expected disconnect."""
|
|
474
|
+
device = create_device_for_command_testing(model)
|
|
475
|
+
device._notifications_enabled = True
|
|
476
|
+
device._expected_disconnect = True
|
|
477
|
+
data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
|
|
478
|
+
with (
|
|
479
|
+
patch.object(device, "_update_lock_status") as mock_update,
|
|
480
|
+
caplog.at_level(logging.DEBUG),
|
|
481
|
+
):
|
|
482
|
+
device._notification_handler(0, data)
|
|
483
|
+
# Should not update lock status during disconnect
|
|
484
|
+
mock_update.assert_not_called()
|
|
485
|
+
# Should log debug message
|
|
486
|
+
assert "Ignoring lock notification during expected disconnect" in caplog.text
|
|
487
|
+
|
|
488
|
+
|
|
481
489
|
@pytest.mark.parametrize(
|
|
482
490
|
"model",
|
|
483
491
|
[
|
|
@@ -30,11 +30,12 @@ def create_device_for_command_testing(
|
|
|
30
30
|
):
|
|
31
31
|
"""Create a device for command testing."""
|
|
32
32
|
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
33
|
-
|
|
34
|
-
relay_switch.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
if model == SwitchbotModel.GARAGE_DOOR_OPENER:
|
|
34
|
+
device_class = relay_switch.SwitchbotGarageDoorOpener
|
|
35
|
+
elif model == SwitchbotModel.RELAY_SWITCH_2PM:
|
|
36
|
+
device_class = relay_switch.SwitchbotRelaySwitch2PM
|
|
37
|
+
else:
|
|
38
|
+
device_class = relay_switch.SwitchbotRelaySwitch
|
|
38
39
|
device = device_class(
|
|
39
40
|
ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=model
|
|
40
41
|
)
|
|
@@ -453,12 +454,49 @@ def test_merge_data(old_data, new_data, expected_result):
|
|
|
453
454
|
assert result == expected_result
|
|
454
455
|
|
|
455
456
|
|
|
457
|
+
@pytest.mark.asyncio
|
|
458
|
+
async def test_garage_door_opener_open():
|
|
459
|
+
"""Test open the garage door."""
|
|
460
|
+
device = create_device_for_command_testing(
|
|
461
|
+
b">\x00\x00\x00", SwitchbotModel.GARAGE_DOOR_OPENER
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
await device.open()
|
|
465
|
+
device._send_command.assert_awaited_once_with(device._open_command)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@pytest.mark.asyncio
|
|
469
|
+
async def test_garage_door_opener_close():
|
|
470
|
+
"""Test close the garage door."""
|
|
471
|
+
device = create_device_for_command_testing(
|
|
472
|
+
b">\x00\x00\x00", SwitchbotModel.GARAGE_DOOR_OPENER
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
await device.close()
|
|
476
|
+
device._send_command.assert_awaited_once_with(device._close_command)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@pytest.mark.parametrize(
|
|
480
|
+
"door_open",
|
|
481
|
+
[
|
|
482
|
+
True,
|
|
483
|
+
False,
|
|
484
|
+
],
|
|
485
|
+
)
|
|
486
|
+
@pytest.mark.asyncio
|
|
487
|
+
async def test_garage_door_opener_door_open(door_open):
|
|
488
|
+
"""Test get garage door state."""
|
|
489
|
+
device = create_device_for_command_testing(
|
|
490
|
+
b">\x00\x00\x00", SwitchbotModel.GARAGE_DOOR_OPENER, {"door_open": door_open}
|
|
491
|
+
)
|
|
492
|
+
assert device.door_open() is door_open
|
|
493
|
+
|
|
494
|
+
|
|
456
495
|
@pytest.mark.asyncio
|
|
457
496
|
async def test_press():
|
|
458
497
|
"""Test the press command for garage door opener."""
|
|
459
498
|
device = create_device_for_command_testing(
|
|
460
499
|
b">\x00\x00\x00", SwitchbotModel.GARAGE_DOOR_OPENER
|
|
461
500
|
)
|
|
462
|
-
|
|
463
501
|
await device.press()
|
|
464
502
|
device._send_command.assert_awaited_once_with(device._press_command)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|