PySwitchbot 2.0.1__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.1 → pyswitchbot-2.1.0}/PKG-INFO +1 -1
  2. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/setup.py +1 -1
  4. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/lock.py +30 -2
  5. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_lock.py +72 -0
  6. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/LICENSE +0 -0
  7. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/MANIFEST.in +0 -0
  8. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/SOURCES.txt +0 -0
  9. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  10. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/requires.txt +0 -0
  11. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  12. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/README.md +0 -0
  13. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/pyproject.toml +0 -0
  14. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/setup.cfg +0 -0
  15. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/__init__.py +0 -0
  16. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parser.py +0 -0
  17. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/__init__.py +0 -0
  18. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  19. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/art_frame.py +0 -0
  20. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  21. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/bot.py +0 -0
  22. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/bulb.py +0 -0
  23. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  24. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/climate_panel.py +0 -0
  25. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/contact.py +0 -0
  26. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/curtain.py +0 -0
  27. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/fan.py +0 -0
  28. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/hub2.py +0 -0
  29. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/hub3.py +0 -0
  30. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  31. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/humidifier.py +0 -0
  32. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/keypad.py +0 -0
  33. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/keypad_vision.py +0 -0
  34. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/leak.py +0 -0
  35. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/light_strip.py +0 -0
  36. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/lock.py +0 -0
  37. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/meter.py +0 -0
  38. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/motion.py +0 -0
  39. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/plug.py +0 -0
  40. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/presence_sensor.py +0 -0
  41. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  42. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/remote.py +0 -0
  43. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  44. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
  45. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/adv_parsers/vacuum.py +0 -0
  46. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/api_config.py +0 -0
  47. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/__init__.py +0 -0
  48. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/air_purifier.py +0 -0
  49. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/climate.py +0 -0
  50. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/evaporative_humidifier.py +0 -0
  51. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/fan.py +0 -0
  52. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/hub2.py +0 -0
  53. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/hub3.py +0 -0
  54. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/light.py +0 -0
  55. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/lock.py +0 -0
  56. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/const/presence_sensor.py +0 -0
  57. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/__init__.py +0 -0
  58. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/air_purifier.py +0 -0
  59. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/art_frame.py +0 -0
  60. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/base_cover.py +0 -0
  61. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/base_light.py +0 -0
  62. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/blind_tilt.py +0 -0
  63. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/bot.py +0 -0
  64. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/bulb.py +0 -0
  65. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/ceiling_light.py +0 -0
  66. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/contact.py +0 -0
  67. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/curtain.py +0 -0
  68. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/device.py +0 -0
  69. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/evaporative_humidifier.py +0 -0
  70. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/fan.py +0 -0
  71. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/humidifier.py +0 -0
  72. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/keypad.py +0 -0
  73. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/keypad_vision.py +0 -0
  74. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/light_strip.py +0 -0
  75. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/meter.py +0 -0
  76. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/meter_pro.py +0 -0
  77. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/motion.py +0 -0
  78. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/plug.py +0 -0
  79. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/relay_switch.py +0 -0
  80. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/roller_shade.py +0 -0
  81. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/smart_thermostat_radiator.py +0 -0
  82. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/devices/vacuum.py +0 -0
  83. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/discovery.py +0 -0
  84. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/enum.py +0 -0
  85. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/helpers.py +0 -0
  86. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/models.py +0 -0
  87. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/switchbot/utils.py +0 -0
  88. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_adv_parser.py +0 -0
  89. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_air_purifier.py +0 -0
  90. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_art_frame.py +0 -0
  91. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_base_cover.py +0 -0
  92. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_blind_tilt.py +0 -0
  93. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_bulb.py +0 -0
  94. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_ceiling_light.py +0 -0
  95. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_colormode_imports.py +0 -0
  96. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_curtain.py +0 -0
  97. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_device.py +0 -0
  98. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_discovery_callback.py +0 -0
  99. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_encrypted_device.py +0 -0
  100. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_evaporative_humidifier.py +0 -0
  101. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_fan.py +0 -0
  102. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_helpers.py +0 -0
  103. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_hub2.py +0 -0
  104. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_hub3.py +0 -0
  105. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_keypad_vision.py +0 -0
  106. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_meter_pro.py +0 -0
  107. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_relay_switch.py +0 -0
  108. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_roller_shade.py +0 -0
  109. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_smart_thermostat_radiator.py +0 -0
  110. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_strip_light.py +0 -0
  111. {pyswitchbot-2.0.1 → pyswitchbot-2.1.0}/tests/test_utils.py +0 -0
  112. {pyswitchbot-2.0.1 → 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.1
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 2.0.1
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
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="2.0.1",
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",
@@ -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
@@ -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
File without changes