PySwitchbot 2.0.0__tar.gz → 2.1.0__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 (112) hide show
  1. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/PKG-INFO +11 -3
  2. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/PKG-INFO +11 -3
  3. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/README.md +10 -2
  4. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/setup.py +1 -1
  5. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/keypad_vision.py +2 -0
  6. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/lock.py +30 -2
  7. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_adv_parser.py +4 -0
  8. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_lock.py +72 -0
  9. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/LICENSE +0 -0
  10. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/MANIFEST.in +0 -0
  11. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/SOURCES.txt +0 -0
  12. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  13. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/requires.txt +0 -0
  14. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  15. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/pyproject.toml +0 -0
  16. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/setup.cfg +0 -0
  17. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/__init__.py +0 -0
  18. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parser.py +0 -0
  19. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/__init__.py +0 -0
  20. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  21. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/art_frame.py +0 -0
  22. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  23. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/bot.py +0 -0
  24. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/bulb.py +0 -0
  25. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  26. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/climate_panel.py +0 -0
  27. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/contact.py +0 -0
  28. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/curtain.py +0 -0
  29. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/fan.py +0 -0
  30. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/hub2.py +0 -0
  31. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/hub3.py +0 -0
  32. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  33. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/humidifier.py +0 -0
  34. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/keypad.py +0 -0
  35. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/leak.py +0 -0
  36. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/light_strip.py +0 -0
  37. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/lock.py +0 -0
  38. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/meter.py +0 -0
  39. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/motion.py +0 -0
  40. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/plug.py +0 -0
  41. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/presence_sensor.py +0 -0
  42. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  43. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/remote.py +0 -0
  44. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  45. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
  46. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/adv_parsers/vacuum.py +0 -0
  47. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/api_config.py +0 -0
  48. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/__init__.py +0 -0
  49. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/air_purifier.py +0 -0
  50. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/climate.py +0 -0
  51. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/evaporative_humidifier.py +0 -0
  52. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/fan.py +0 -0
  53. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/hub2.py +0 -0
  54. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/hub3.py +0 -0
  55. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/light.py +0 -0
  56. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/lock.py +0 -0
  57. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/const/presence_sensor.py +0 -0
  58. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/__init__.py +0 -0
  59. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/air_purifier.py +0 -0
  60. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/art_frame.py +0 -0
  61. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/base_cover.py +0 -0
  62. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/base_light.py +0 -0
  63. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/blind_tilt.py +0 -0
  64. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/bot.py +0 -0
  65. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/bulb.py +0 -0
  66. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/ceiling_light.py +0 -0
  67. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/contact.py +0 -0
  68. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/curtain.py +0 -0
  69. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/device.py +0 -0
  70. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/evaporative_humidifier.py +0 -0
  71. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/fan.py +0 -0
  72. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/humidifier.py +0 -0
  73. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/keypad.py +0 -0
  74. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/keypad_vision.py +0 -0
  75. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/light_strip.py +0 -0
  76. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/meter.py +0 -0
  77. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/meter_pro.py +0 -0
  78. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/motion.py +0 -0
  79. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/plug.py +0 -0
  80. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/relay_switch.py +0 -0
  81. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/roller_shade.py +0 -0
  82. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/smart_thermostat_radiator.py +0 -0
  83. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/devices/vacuum.py +0 -0
  84. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/discovery.py +0 -0
  85. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/enum.py +0 -0
  86. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/helpers.py +0 -0
  87. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/models.py +0 -0
  88. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/switchbot/utils.py +0 -0
  89. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_air_purifier.py +0 -0
  90. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_art_frame.py +0 -0
  91. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_base_cover.py +0 -0
  92. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_blind_tilt.py +0 -0
  93. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_bulb.py +0 -0
  94. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_ceiling_light.py +0 -0
  95. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_colormode_imports.py +0 -0
  96. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_curtain.py +0 -0
  97. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_device.py +0 -0
  98. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_discovery_callback.py +0 -0
  99. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_encrypted_device.py +0 -0
  100. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_evaporative_humidifier.py +0 -0
  101. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_fan.py +0 -0
  102. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_helpers.py +0 -0
  103. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_hub2.py +0 -0
  104. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_hub3.py +0 -0
  105. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_keypad_vision.py +0 -0
  106. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_meter_pro.py +0 -0
  107. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_relay_switch.py +0 -0
  108. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_roller_shade.py +0 -0
  109. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_smart_thermostat_radiator.py +0 -0
  110. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_strip_light.py +0 -0
  111. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_utils.py +0 -0
  112. {pyswitchbot-2.0.0 → pyswitchbot-2.1.0}/tests/test_vacuum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/sblibs/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -33,7 +33,15 @@ Dynamic: summary
33
33
 
34
34
  # pySwitchbot [![codecov](https://codecov.io/gh/sblibs/pySwitchbot/graph/badge.svg?token=TI027U5ISQ)](https://codecov.io/gh/sblibs/pySwitchbot)
35
35
 
36
- Library to control Switchbot IoT devices https://www.switch-bot.com/bot
36
+ Library to control Switchbot IoT devices https://www.switch-bot.com/
37
+
38
+ ## Setting up the environment
39
+
40
+ ```shell
41
+ python3 -m venv .venv
42
+ source .venv/bin/activate
43
+ pip install .
44
+ ```
37
45
 
38
46
  ## Obtaining encryption key for Switchbot Locks
39
47
 
@@ -42,7 +50,7 @@ Using the script `scripts/get_encryption_key.py` you can manually obtain locks e
42
50
  Usage:
43
51
 
44
52
  ```shell
45
- $ python3 get_encryption_key.py MAC USERNAME
53
+ $ python3 scripts/get_encryption_key.py MAC USERNAME
46
54
  Key ID: XX
47
55
  Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
48
56
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/sblibs/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -33,7 +33,15 @@ Dynamic: summary
33
33
 
34
34
  # pySwitchbot [![codecov](https://codecov.io/gh/sblibs/pySwitchbot/graph/badge.svg?token=TI027U5ISQ)](https://codecov.io/gh/sblibs/pySwitchbot)
35
35
 
36
- Library to control Switchbot IoT devices https://www.switch-bot.com/bot
36
+ Library to control Switchbot IoT devices https://www.switch-bot.com/
37
+
38
+ ## Setting up the environment
39
+
40
+ ```shell
41
+ python3 -m venv .venv
42
+ source .venv/bin/activate
43
+ pip install .
44
+ ```
37
45
 
38
46
  ## Obtaining encryption key for Switchbot Locks
39
47
 
@@ -42,7 +50,7 @@ Using the script `scripts/get_encryption_key.py` you can manually obtain locks e
42
50
  Usage:
43
51
 
44
52
  ```shell
45
- $ python3 get_encryption_key.py MAC USERNAME
53
+ $ python3 scripts/get_encryption_key.py MAC USERNAME
46
54
  Key ID: XX
47
55
  Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
48
56
  ```
@@ -1,6 +1,14 @@
1
1
  # pySwitchbot [![codecov](https://codecov.io/gh/sblibs/pySwitchbot/graph/badge.svg?token=TI027U5ISQ)](https://codecov.io/gh/sblibs/pySwitchbot)
2
2
 
3
- Library to control Switchbot IoT devices https://www.switch-bot.com/bot
3
+ Library to control Switchbot IoT devices https://www.switch-bot.com/
4
+
5
+ ## Setting up the environment
6
+
7
+ ```shell
8
+ python3 -m venv .venv
9
+ source .venv/bin/activate
10
+ pip install .
11
+ ```
4
12
 
5
13
  ## Obtaining encryption key for Switchbot Locks
6
14
 
@@ -9,7 +17,7 @@ Using the script `scripts/get_encryption_key.py` you can manually obtain locks e
9
17
  Usage:
10
18
 
11
19
  ```shell
12
- $ python3 get_encryption_key.py MAC USERNAME
20
+ $ python3 scripts/get_encryption_key.py MAC USERNAME
13
21
  Key ID: XX
14
22
  Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
15
23
  ```
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="2.0.0",
23
+ version="2.1.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -19,6 +19,7 @@ def process_common_mfr_data(mfr_data: bytes | None) -> dict[str, bool | int]:
19
19
  low_temperature = bool(mfr_data[8] & 0b10000000)
20
20
  high_temperature = bool(mfr_data[8] & 0b01000000)
21
21
  doorbell = bool(mfr_data[12] & 0b00001000)
22
+ doorbell_seq = mfr_data[12] & 0b00000111
22
23
 
23
24
  return {
24
25
  "sequence_number": sequence_number,
@@ -30,6 +31,7 @@ def process_common_mfr_data(mfr_data: bytes | None) -> dict[str, bool | int]:
30
31
  "low_temperature": low_temperature,
31
32
  "high_temperature": high_temperature,
32
33
  "doorbell": doorbell,
34
+ "doorbell_seq": doorbell_seq,
33
35
  }
34
36
 
35
37
 
@@ -10,7 +10,11 @@ from bleak.backends.device import BLEDevice
10
10
 
11
11
  from ..const import SwitchbotModel
12
12
  from ..const.lock import LockStatus
13
- from .device import SwitchbotEncryptedDevice, SwitchbotSequenceDevice
13
+ from .device import (
14
+ SwitchbotEncryptedDevice,
15
+ SwitchbotOperationError,
16
+ SwitchbotSequenceDevice,
17
+ )
14
18
 
15
19
  COMMAND_HEADER = "57"
16
20
  COMMAND_LOCK_INFO = {
@@ -49,6 +53,10 @@ COMMAND_LOCK = {
49
53
  SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4e0101000000",
50
54
  SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4e0101000000",
51
55
  }
56
+ COMMAND_HALF_LOCK = {
57
+ SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e0101000008",
58
+ }
59
+
52
60
  COMMAND_ENABLE_NOTIFICATIONS = {
53
61
  SwitchbotModel.LOCK: f"{COMMAND_HEADER}0e01001e00008101",
54
62
  SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0e01001e00008101",
@@ -129,6 +137,19 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
129
137
  {LockStatus.UNLOCKED, LockStatus.UNLOCKING, LockStatus.NOT_FULLY_LOCKED},
130
138
  )
131
139
 
140
+ async def half_lock(self) -> bool:
141
+ """Send half lock command (Lock Ultra EU type only)."""
142
+ if self._model not in COMMAND_HALF_LOCK:
143
+ raise SwitchbotOperationError(
144
+ f"Half lock is not supported on {self._model}"
145
+ )
146
+ if not self.is_half_lock_calibrated():
147
+ raise SwitchbotOperationError("Half lock is not calibrated")
148
+ return await self._lock_unlock(
149
+ COMMAND_HALF_LOCK[self._model],
150
+ {LockStatus.HALF_LOCKED, LockStatus.LOCKING},
151
+ )
152
+
132
153
  def _parse_basic_data(self, basic_data: bytes) -> dict[str, Any]:
133
154
  """Parse basic data from lock."""
134
155
  return {
@@ -207,6 +228,10 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
207
228
  """Return True if Night Latch is enabled on EU firmware."""
208
229
  return self._get_adv_value("night_latch")
209
230
 
231
+ def is_half_lock_calibrated(self) -> bool | None:
232
+ """Return True if half lock position is calibrated (Lock Ultra only)."""
233
+ return self._get_adv_value("half_lock_calibration")
234
+
210
235
  async def _get_lock_info(self) -> bytes | None:
211
236
  """Return lock info of device."""
212
237
  _data = await self._send_command(
@@ -268,10 +293,13 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
268
293
  "status": LockStatus((data[0] & 0b01110000) >> 4),
269
294
  "unlocked_alarm": bool(data[1] & 0b00010000),
270
295
  }
271
- return {
296
+ result = {
272
297
  "calibration": bool(data[0] & 0b10000000),
273
298
  "status": LockStatus((data[0] & 0b01111000) >> 3),
274
299
  "door_open": bool(data[1] & 0b00010000),
275
300
  "unclosed_alarm": bool(data[5] & 0b10000000),
276
301
  "unlocked_alarm": bool(data[5] & 0b01000000),
277
302
  }
303
+ if model is SwitchbotModel.LOCK_ULTRA:
304
+ result["half_lock_calibration"] = bool(data[1] & 0b00000001)
305
+ return result
@@ -3718,6 +3718,7 @@ def test_humidifer_with_empty_data() -> None:
3718
3718
  "battery": 95,
3719
3719
  "battery_charging": True,
3720
3720
  "doorbell": False,
3721
+ "doorbell_seq": 0,
3721
3722
  "duress_alarm": False,
3722
3723
  "high_temperature": False,
3723
3724
  "lockout_alarm": False,
@@ -3737,6 +3738,7 @@ def test_humidifer_with_empty_data() -> None:
3737
3738
  "battery": 96,
3738
3739
  "battery_charging": False,
3739
3740
  "doorbell": False,
3741
+ "doorbell_seq": 0,
3740
3742
  "duress_alarm": False,
3741
3743
  "high_temperature": False,
3742
3744
  "lockout_alarm": False,
@@ -4057,6 +4059,7 @@ def test_adv_active(test_case: AdvTestCase) -> None:
4057
4059
  "battery": 95,
4058
4060
  "battery_charging": True,
4059
4061
  "doorbell": False,
4062
+ "doorbell_seq": 0,
4060
4063
  "duress_alarm": False,
4061
4064
  "high_temperature": False,
4062
4065
  "lockout_alarm": False,
@@ -4076,6 +4079,7 @@ def test_adv_active(test_case: AdvTestCase) -> None:
4076
4079
  "battery": 96,
4077
4080
  "battery_charging": False,
4078
4081
  "doorbell": False,
4082
+ "doorbell_seq": 0,
4079
4083
  "duress_alarm": False,
4080
4084
  "high_temperature": False,
4081
4085
  "lockout_alarm": False,
@@ -6,6 +6,7 @@ import pytest
6
6
  from switchbot import SwitchbotModel
7
7
  from switchbot.const.lock import LockStatus
8
8
  from switchbot.devices import lock
9
+ from switchbot.devices.device import SwitchbotOperationError
9
10
 
10
11
  from .test_adv_parser import generate_ble_device
11
12
 
@@ -624,6 +625,7 @@ def test_update_lock_status(model: str):
624
625
  "door_open": True,
625
626
  "unclosed_alarm": True,
626
627
  "unlocked_alarm": True,
628
+ "half_lock_calibration": False,
627
629
  },
628
630
  ),
629
631
  (
@@ -712,6 +714,7 @@ def test_parse_lock_data(model: str, data: bytes, expected: dict):
712
714
  "door_open": False,
713
715
  "unclosed_alarm": False,
714
716
  "unlocked_alarm": True, # bit 6 of byte 5
717
+ "half_lock_calibration": False,
715
718
  },
716
719
  ),
717
720
  (
@@ -760,6 +763,75 @@ async def test_lock_with_update(model: str):
760
763
  assert result is True
761
764
 
762
765
 
766
+ def test_is_half_lock_calibrated():
767
+ """Test is_half_lock_calibrated method."""
768
+ device = create_device_for_command_testing(SwitchbotModel.LOCK_ULTRA)
769
+ device._get_adv_value = Mock(return_value=True)
770
+ assert device.is_half_lock_calibrated() is True
771
+
772
+ device._get_adv_value = Mock(return_value=False)
773
+ assert device.is_half_lock_calibrated() is False
774
+
775
+
776
+ @pytest.mark.asyncio
777
+ async def test_half_lock_calibrated():
778
+ """Test half_lock succeeds when calibrated."""
779
+ device = create_device_for_command_testing(SwitchbotModel.LOCK_ULTRA)
780
+ device._get_adv_value = Mock(side_effect=[True, LockStatus.LOCKED])
781
+ with (
782
+ patch.object(device, "_send_command", return_value=b"\x01\x00"),
783
+ patch.object(device, "_enable_notifications", return_value=True),
784
+ patch.object(device, "_get_basic_info", return_value=b"\x01\x64\x01"),
785
+ ):
786
+ result = await device.half_lock()
787
+ assert result is True
788
+
789
+
790
+ @pytest.mark.asyncio
791
+ async def test_half_lock_not_calibrated():
792
+ """Test half_lock raises SwitchbotOperationError when not calibrated."""
793
+ device = create_device_for_command_testing(SwitchbotModel.LOCK_ULTRA)
794
+ device._get_adv_value = Mock(return_value=False)
795
+ with pytest.raises(SwitchbotOperationError, match="not calibrated"):
796
+ await device.half_lock()
797
+
798
+
799
+ @pytest.mark.asyncio
800
+ @pytest.mark.parametrize(
801
+ "model",
802
+ [
803
+ SwitchbotModel.LOCK,
804
+ SwitchbotModel.LOCK_LITE,
805
+ SwitchbotModel.LOCK_PRO,
806
+ SwitchbotModel.LOCK_VISION,
807
+ SwitchbotModel.LOCK_VISION_PRO,
808
+ SwitchbotModel.LOCK_PRO_WIFI,
809
+ ],
810
+ )
811
+ async def test_half_lock_unsupported_model(model: str):
812
+ """Test half_lock raises SwitchbotOperationError on unsupported models."""
813
+ device = create_device_for_command_testing(model)
814
+ with pytest.raises(SwitchbotOperationError, match="not supported"):
815
+ await device.half_lock()
816
+
817
+
818
+ @pytest.mark.asyncio
819
+ async def test_half_lock():
820
+ """Test half_lock method."""
821
+ device = create_device_for_command_testing(SwitchbotModel.LOCK_ULTRA)
822
+ device._get_adv_value = Mock(side_effect=[True, LockStatus.LOCKED])
823
+ with (
824
+ patch.object(device, "_send_command", return_value=b"\x01\x00") as mock_send,
825
+ patch.object(device, "_enable_notifications", return_value=True),
826
+ patch.object(device, "_get_basic_info", return_value=b"\x01\x64\x01"),
827
+ ):
828
+ result = await device.half_lock()
829
+ assert result is True
830
+ mock_send.assert_awaited_once_with(
831
+ lock.COMMAND_HALF_LOCK[SwitchbotModel.LOCK_ULTRA]
832
+ )
833
+
834
+
763
835
  @pytest.mark.asyncio
764
836
  @pytest.mark.parametrize(
765
837
  ("model", "status"),
File without changes
File without changes
File without changes
File without changes