PySwitchbot 0.62.2__tar.gz → 0.63.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 (85) hide show
  1. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/PySwitchbot.egg-info/SOURCES.txt +3 -0
  4. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/setup.py +1 -1
  5. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parser.py +14 -1
  6. pyswitchbot-0.63.0/switchbot/adv_parsers/hub3.py +56 -0
  7. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/const/__init__.py +1 -0
  8. pyswitchbot-0.63.0/switchbot/const/hub3.py +18 -0
  9. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_adv_parser.py +100 -0
  10. pyswitchbot-0.63.0/tests/test_hub3.py +13 -0
  11. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/LICENSE +0 -0
  12. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/MANIFEST.in +0 -0
  13. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  14. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/PySwitchbot.egg-info/requires.txt +0 -0
  15. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  16. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/README.md +0 -0
  17. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/pyproject.toml +0 -0
  18. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/setup.cfg +0 -0
  19. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/__init__.py +0 -0
  20. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/__init__.py +0 -0
  21. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  22. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  23. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/bot.py +0 -0
  24. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/bulb.py +0 -0
  25. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  26. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/contact.py +0 -0
  27. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/curtain.py +0 -0
  28. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/fan.py +0 -0
  29. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/hub2.py +0 -0
  30. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  31. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/humidifier.py +0 -0
  32. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/keypad.py +0 -0
  33. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/leak.py +0 -0
  34. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/light_strip.py +0 -0
  35. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/lock.py +0 -0
  36. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/meter.py +0 -0
  37. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/motion.py +0 -0
  38. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/plug.py +0 -0
  39. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  40. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/remote.py +0 -0
  41. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  42. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/adv_parsers/vacuum.py +0 -0
  43. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/api_config.py +0 -0
  44. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/const/air_purifier.py +0 -0
  45. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/const/evaporative_humidifier.py +0 -0
  46. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/const/fan.py +0 -0
  47. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/const/hub2.py +0 -0
  48. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/const/lock.py +0 -0
  49. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/__init__.py +0 -0
  50. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/air_purifier.py +0 -0
  51. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/base_cover.py +0 -0
  52. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/base_light.py +0 -0
  53. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/blind_tilt.py +0 -0
  54. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/bot.py +0 -0
  55. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/bulb.py +0 -0
  56. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/ceiling_light.py +0 -0
  57. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/contact.py +0 -0
  58. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/curtain.py +0 -0
  59. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/device.py +0 -0
  60. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/evaporative_humidifier.py +0 -0
  61. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/fan.py +0 -0
  62. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/humidifier.py +0 -0
  63. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/keypad.py +0 -0
  64. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/light_strip.py +0 -0
  65. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/lock.py +0 -0
  66. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/meter.py +0 -0
  67. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/motion.py +0 -0
  68. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/plug.py +0 -0
  69. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/relay_switch.py +0 -0
  70. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/roller_shade.py +0 -0
  71. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/devices/vacuum.py +0 -0
  72. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/discovery.py +0 -0
  73. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/enum.py +0 -0
  74. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/helpers.py +0 -0
  75. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/switchbot/models.py +0 -0
  76. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_air_purifier.py +0 -0
  77. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_base_cover.py +0 -0
  78. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_blind_tilt.py +0 -0
  79. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_curtain.py +0 -0
  80. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_evaporative_humidifier.py +0 -0
  81. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_fan.py +0 -0
  82. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_hub2.py +0 -0
  83. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_relay_switch.py +0 -0
  84. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_roller_shade.py +0 -0
  85. {pyswitchbot-0.62.2 → pyswitchbot-0.63.0}/tests/test_vacuum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.62.2
3
+ Version: 0.63.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: 0.62.2
3
+ Version: 0.63.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
@@ -25,6 +25,7 @@ switchbot/adv_parsers/contact.py
25
25
  switchbot/adv_parsers/curtain.py
26
26
  switchbot/adv_parsers/fan.py
27
27
  switchbot/adv_parsers/hub2.py
28
+ switchbot/adv_parsers/hub3.py
28
29
  switchbot/adv_parsers/hubmini_matter.py
29
30
  switchbot/adv_parsers/humidifier.py
30
31
  switchbot/adv_parsers/keypad.py
@@ -43,6 +44,7 @@ switchbot/const/air_purifier.py
43
44
  switchbot/const/evaporative_humidifier.py
44
45
  switchbot/const/fan.py
45
46
  switchbot/const/hub2.py
47
+ switchbot/const/hub3.py
46
48
  switchbot/const/lock.py
47
49
  switchbot/devices/__init__.py
48
50
  switchbot/devices/air_purifier.py
@@ -75,6 +77,7 @@ tests/test_curtain.py
75
77
  tests/test_evaporative_humidifier.py
76
78
  tests/test_fan.py
77
79
  tests/test_hub2.py
80
+ tests/test_hub3.py
78
81
  tests/test_relay_switch.py
79
82
  tests/test_roller_shade.py
80
83
  tests/test_vacuum.py
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="0.62.2",
23
+ version="0.63.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 @@ from .adv_parsers.contact import process_wocontact
19
19
  from .adv_parsers.curtain import process_wocurtain
20
20
  from .adv_parsers.fan import process_fan
21
21
  from .adv_parsers.hub2 import process_wohub2
22
+ from .adv_parsers.hub3 import process_hub3
22
23
  from .adv_parsers.hubmini_matter import process_hubmini_matter
23
24
  from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
24
25
  from .adv_parsers.keypad import process_wokeypad
@@ -57,7 +58,7 @@ class SwitchbotSupportedType(TypedDict):
57
58
  manufacturer_data_length: int | None
58
59
 
59
60
 
60
- SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
61
+ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
61
62
  "d": {
62
63
  "modelName": SwitchbotModel.CONTACT_SENSOR,
63
64
  "modelFriendlyName": "Contact Sensor",
@@ -293,6 +294,12 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
293
294
  "func": process_air_purifier,
294
295
  "manufacturer_id": 2409,
295
296
  },
297
+ b"\x00\x10\xb9\x40": {
298
+ "modelName": SwitchbotModel.HUB3,
299
+ "modelFriendlyName": "Hub3",
300
+ "func": process_hub3,
301
+ "manufacturer_id": 2409,
302
+ },
296
303
  }
297
304
 
298
305
  _SWITCHBOT_MODEL_TO_CHAR = {
@@ -371,6 +378,12 @@ def _parse_data(
371
378
  if model_data.get("manufacturer_data_length") == len(_mfr_data):
372
379
  _model = model_chr
373
380
  break
381
+ if (
382
+ _service_data
383
+ and len(_service_data) > 5
384
+ and _service_data[-4:] in SUPPORTED_TYPES
385
+ ):
386
+ _model = _service_data[-4:]
374
387
 
375
388
  if not _model:
376
389
  return None
@@ -0,0 +1,56 @@
1
+ """Air Purifier adv parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..const.hub3 import LIGHT_INTENSITY_MAP
8
+
9
+
10
+ def process_hub3(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
11
+ """Process hub3 sensor manufacturer data."""
12
+ if mfr_data is None:
13
+ return {}
14
+ device_data = mfr_data[6:]
15
+
16
+ seq_num = device_data[0]
17
+ network_state = (device_data[6] & 0b11000000) >> 6
18
+ sensor_inserted = not bool(device_data[6] & 0b00100000)
19
+ light_level = device_data[6] & 0b00001111
20
+ illuminance = calculate_light_intensity(light_level)
21
+ temperature_alarm = bool(device_data[7] & 0b11000000)
22
+ humidity_alarm = bool(device_data[7] & 0b00110000)
23
+
24
+ temp_data = device_data[7:10]
25
+ _temp_sign = 1 if temp_data[1] & 0b10000000 else -1
26
+ _temp_c = _temp_sign * (
27
+ (temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
28
+ )
29
+ _temp_f = round(((_temp_c * 9 / 5) + 32), 1)
30
+ humidity = temp_data[2] & 0b01111111
31
+ motion_detected = bool(device_data[10] & 0b10000000)
32
+
33
+ return {
34
+ "sequence_number": seq_num,
35
+ "network_state": network_state,
36
+ "sensor_inserted": sensor_inserted,
37
+ "lightLevel": light_level,
38
+ "illuminance": illuminance,
39
+ "temperature_alarm": temperature_alarm,
40
+ "humidity_alarm": humidity_alarm,
41
+ "temp": {"c": _temp_c, "f": _temp_f},
42
+ "temperature": _temp_c,
43
+ "humidity": humidity,
44
+ "motion_detected": motion_detected,
45
+ }
46
+
47
+
48
+ def calculate_light_intensity(light_level: int) -> int:
49
+ """
50
+ Convert Hub 3 light level (1-10) to actual light intensity value
51
+ Args:
52
+ light_level: Integer from 1-10
53
+ Returns:
54
+ Corresponding light intensity value or 0 if invalid input
55
+ """
56
+ return LIGHT_INTENSITY_MAP.get(max(0, min(light_level, 10)), 0)
@@ -75,6 +75,7 @@ class SwitchbotModel(StrEnum):
75
75
  K10_PRO_COMBO_VACUUM = "K10+ Pro Combo Vacuum"
76
76
  AIR_PURIFIER = "Air Purifier"
77
77
  AIR_PURIFIER_TABLE = "Air Purifier Table"
78
+ HUB3 = "Hub3"
78
79
 
79
80
 
80
81
  __all__ = [
@@ -0,0 +1,18 @@
1
+ """
2
+ Mapping of light levels to lux measurement values for SwitchBot Hub 3.
3
+
4
+ Source: After-sales consultation, line chart data provided by switchbot developers
5
+ """
6
+
7
+ LIGHT_INTENSITY_MAP = {
8
+ 1: 0,
9
+ 2: 50,
10
+ 3: 90,
11
+ 4: 205,
12
+ 5: 317,
13
+ 6: 510,
14
+ 7: 610,
15
+ 8: 707,
16
+ 9: 801,
17
+ 10: 1023,
18
+ }
@@ -2842,3 +2842,103 @@ def test_air_purifier_with_empty_data() -> None:
2842
2842
  rssi=-97,
2843
2843
  active=True,
2844
2844
  )
2845
+
2846
+
2847
+ def test_hub3_active() -> None:
2848
+ """Test parsing hub3 with active data."""
2849
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
2850
+ adv_data = generate_advertisement_data(
2851
+ manufacturer_data={2409: b"\xb0\xe9\xfen^)\x00\xffh&\xd6d\x83\x03\x994\x80"},
2852
+ service_data={
2853
+ "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xb9@"
2854
+ },
2855
+ rssi=-97,
2856
+ )
2857
+ result = parse_advertisement_data(ble_device, adv_data)
2858
+ assert result == SwitchBotAdvertisement(
2859
+ address="aa:bb:cc:dd:ee:ff",
2860
+ data={
2861
+ "rawAdvData": b"\x00\x00d\x00\x10\xb9@",
2862
+ "data": {
2863
+ "sequence_number": 0,
2864
+ "network_state": 2,
2865
+ "sensor_inserted": True,
2866
+ "lightLevel": 3,
2867
+ "illuminance": 90,
2868
+ "temperature_alarm": False,
2869
+ "humidity_alarm": False,
2870
+ "temp": {"c": 25.3, "f": 77.5},
2871
+ "temperature": 25.3,
2872
+ "humidity": 52,
2873
+ "motion_detected": True,
2874
+ },
2875
+ "isEncrypted": False,
2876
+ "model": b"\x00\x10\xb9@",
2877
+ "modelFriendlyName": "Hub3",
2878
+ "modelName": SwitchbotModel.HUB3,
2879
+ },
2880
+ device=ble_device,
2881
+ rssi=-97,
2882
+ active=True,
2883
+ )
2884
+
2885
+
2886
+ def test_hub3_passive() -> None:
2887
+ """Test parsing hub3 with passive data."""
2888
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
2889
+ adv_data = generate_advertisement_data(
2890
+ manufacturer_data={2409: b"\xb0\xe9\xfen^)\x00\xffh&\xd6d\x83\x03\x994\x80"},
2891
+ rssi=-97,
2892
+ )
2893
+ result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.HUB3)
2894
+ assert result == SwitchBotAdvertisement(
2895
+ address="aa:bb:cc:dd:ee:ff",
2896
+ data={
2897
+ "rawAdvData": None,
2898
+ "data": {
2899
+ "sequence_number": 0,
2900
+ "network_state": 2,
2901
+ "sensor_inserted": True,
2902
+ "lightLevel": 3,
2903
+ "illuminance": 90,
2904
+ "temperature_alarm": False,
2905
+ "humidity_alarm": False,
2906
+ "temp": {"c": 25.3, "f": 77.5},
2907
+ "temperature": 25.3,
2908
+ "humidity": 52,
2909
+ "motion_detected": True,
2910
+ },
2911
+ "isEncrypted": False,
2912
+ "model": b"\x00\x10\xb9@",
2913
+ "modelFriendlyName": "Hub3",
2914
+ "modelName": SwitchbotModel.HUB3,
2915
+ },
2916
+ device=ble_device,
2917
+ rssi=-97,
2918
+ active=False,
2919
+ )
2920
+
2921
+
2922
+ def test_hub3_with_empty_data() -> None:
2923
+ """Test parsing hub3 with empty data."""
2924
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
2925
+ adv_data = generate_advertisement_data(
2926
+ manufacturer_data={2409: None},
2927
+ service_data={
2928
+ "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xb9@"
2929
+ },
2930
+ rssi=-97,
2931
+ )
2932
+ result = parse_advertisement_data(ble_device, adv_data)
2933
+ assert result == SwitchBotAdvertisement(
2934
+ address="aa:bb:cc:dd:ee:ff",
2935
+ data={
2936
+ "rawAdvData": b"\x00\x00d\x00\x10\xb9@",
2937
+ "data": {},
2938
+ "isEncrypted": False,
2939
+ "model": b"\x00\x10\xb9@",
2940
+ },
2941
+ device=ble_device,
2942
+ rssi=-97,
2943
+ active=True,
2944
+ )
@@ -0,0 +1,13 @@
1
+ from switchbot.adv_parsers.hub3 import calculate_light_intensity
2
+
3
+
4
+ def test_calculate_light_intensity():
5
+ """Test calculating light intensity from Hub 3 light level."""
6
+ # Test valid inputs
7
+ assert calculate_light_intensity(1) == 0
8
+ assert calculate_light_intensity(2) == 50
9
+ assert calculate_light_intensity(5) == 317
10
+ assert calculate_light_intensity(10) == 1023
11
+
12
+ # Test invalid inputs
13
+ assert calculate_light_intensity(0) == 0
File without changes
File without changes
File without changes
File without changes