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.
Files changed (93) hide show
  1. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PKG-INFO +1 -1
  2. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/setup.py +1 -1
  4. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/__init__.py +6 -1
  5. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/device.py +22 -0
  6. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/lock.py +15 -8
  7. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/relay_switch.py +23 -1
  8. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_encrypted_device.py +19 -0
  9. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_lock.py +30 -22
  10. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_relay_switch.py +44 -6
  11. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/LICENSE +0 -0
  12. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/MANIFEST.in +0 -0
  13. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/SOURCES.txt +0 -0
  14. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  15. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/requires.txt +0 -0
  16. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/PySwitchbot.egg-info/top_level.txt +0 -0
  17. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/README.md +0 -0
  18. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/pyproject.toml +0 -0
  19. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/setup.cfg +0 -0
  20. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parser.py +0 -0
  21. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/__init__.py +0 -0
  22. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/air_purifier.py +0 -0
  23. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/blind_tilt.py +0 -0
  24. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/bot.py +0 -0
  25. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/bulb.py +0 -0
  26. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/ceiling_light.py +0 -0
  27. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/contact.py +0 -0
  28. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/curtain.py +0 -0
  29. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/fan.py +0 -0
  30. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/hub2.py +0 -0
  31. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/hub3.py +0 -0
  32. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  33. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/humidifier.py +0 -0
  34. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/keypad.py +0 -0
  35. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/leak.py +0 -0
  36. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/light_strip.py +0 -0
  37. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/lock.py +0 -0
  38. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/meter.py +0 -0
  39. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/motion.py +0 -0
  40. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/plug.py +0 -0
  41. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/relay_switch.py +0 -0
  42. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/remote.py +0 -0
  43. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/roller_shade.py +0 -0
  44. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/adv_parsers/vacuum.py +0 -0
  45. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/api_config.py +0 -0
  46. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/__init__.py +0 -0
  47. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/air_purifier.py +0 -0
  48. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/evaporative_humidifier.py +0 -0
  49. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/fan.py +0 -0
  50. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/hub2.py +0 -0
  51. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/hub3.py +0 -0
  52. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/light.py +0 -0
  53. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/const/lock.py +0 -0
  54. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/__init__.py +0 -0
  55. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/air_purifier.py +0 -0
  56. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/base_cover.py +0 -0
  57. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/base_light.py +0 -0
  58. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/blind_tilt.py +0 -0
  59. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/bot.py +0 -0
  60. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/bulb.py +0 -0
  61. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/ceiling_light.py +0 -0
  62. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/contact.py +0 -0
  63. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/curtain.py +0 -0
  64. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/evaporative_humidifier.py +0 -0
  65. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/fan.py +0 -0
  66. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/humidifier.py +0 -0
  67. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/keypad.py +0 -0
  68. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/light_strip.py +0 -0
  69. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/meter.py +0 -0
  70. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/motion.py +0 -0
  71. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/plug.py +0 -0
  72. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/roller_shade.py +0 -0
  73. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/devices/vacuum.py +0 -0
  74. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/discovery.py +0 -0
  75. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/enum.py +0 -0
  76. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/helpers.py +0 -0
  77. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/switchbot/models.py +0 -0
  78. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_adv_parser.py +0 -0
  79. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_air_purifier.py +0 -0
  80. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_base_cover.py +0 -0
  81. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_blind_tilt.py +0 -0
  82. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_bulb.py +0 -0
  83. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_ceiling_light.py +0 -0
  84. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_colormode_imports.py +0 -0
  85. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_curtain.py +0 -0
  86. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_evaporative_humidifier.py +0 -0
  87. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_fan.py +0 -0
  88. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_helpers.py +0 -0
  89. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_hub2.py +0 -0
  90. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_hub3.py +0 -0
  91. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_roller_shade.py +0 -0
  92. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_strip_light.py +0 -0
  93. {pyswitchbot-0.68.0 → pyswitchbot-0.68.2}/tests/test_vacuum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.68.0
3
+ Version: 0.68.2
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/sblibs/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.68.0
3
+ Version: 0.68.2
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/sblibs/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="0.68.0",
23
+ version="0.68.2",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -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 SwitchbotRelaySwitch, SwitchbotRelaySwitch2PM
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 = f"{COMMAND_HEADER}0e01001e00008101"
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
- if self._notifications_enabled:
201
- return True
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._notifications_enabled and self._check_command_result(data, 0, {0xF}):
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"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
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
- device_class = (
34
- relay_switch.SwitchbotRelaySwitch2PM
35
- if model == SwitchbotModel.RELAY_SWITCH_2PM
36
- else relay_switch.SwitchbotRelaySwitch
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