PySwitchbot 0.74.0__tar.gz → 0.76.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 (109) hide show
  1. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/SOURCES.txt +6 -0
  4. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/setup.py +1 -1
  5. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/__init__.py +4 -0
  6. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parser.py +38 -0
  7. pyswitchbot-0.76.0/switchbot/adv_parsers/art_frame.py +35 -0
  8. pyswitchbot-0.76.0/switchbot/adv_parsers/keypad_vision.py +79 -0
  9. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/presence_sensor.py +3 -1
  10. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/__init__.py +3 -0
  11. pyswitchbot-0.76.0/switchbot/devices/art_frame.py +140 -0
  12. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/base_cover.py +0 -13
  13. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/base_light.py +0 -13
  14. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/device.py +32 -5
  15. pyswitchbot-0.76.0/switchbot/devices/keypad_vision.py +167 -0
  16. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_adv_parser.py +136 -0
  17. pyswitchbot-0.76.0/tests/test_art_frame.py +221 -0
  18. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_device.py +39 -2
  19. pyswitchbot-0.76.0/tests/test_keypad_vision.py +259 -0
  20. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/LICENSE +0 -0
  21. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/MANIFEST.in +0 -0
  22. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  23. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/requires.txt +0 -0
  24. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  25. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/README.md +0 -0
  26. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/pyproject.toml +0 -0
  27. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/setup.cfg +0 -0
  28. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/__init__.py +0 -0
  29. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  30. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  31. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/bot.py +0 -0
  32. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/bulb.py +0 -0
  33. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  34. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/climate_panel.py +0 -0
  35. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/contact.py +0 -0
  36. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/curtain.py +0 -0
  37. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/fan.py +0 -0
  38. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/hub2.py +0 -0
  39. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/hub3.py +0 -0
  40. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  41. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/humidifier.py +0 -0
  42. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/keypad.py +0 -0
  43. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/leak.py +0 -0
  44. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/light_strip.py +0 -0
  45. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/lock.py +0 -0
  46. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/meter.py +0 -0
  47. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/motion.py +0 -0
  48. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/plug.py +0 -0
  49. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  50. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/remote.py +0 -0
  51. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  52. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
  53. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/vacuum.py +0 -0
  54. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/api_config.py +0 -0
  55. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/air_purifier.py +0 -0
  56. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/climate.py +0 -0
  57. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/evaporative_humidifier.py +0 -0
  58. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/fan.py +0 -0
  59. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/hub2.py +0 -0
  60. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/hub3.py +0 -0
  61. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/light.py +0 -0
  62. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/lock.py +0 -0
  63. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/presence_sensor.py +0 -0
  64. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/__init__.py +0 -0
  65. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/air_purifier.py +0 -0
  66. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/blind_tilt.py +0 -0
  67. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/bot.py +0 -0
  68. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/bulb.py +0 -0
  69. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/ceiling_light.py +0 -0
  70. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/contact.py +0 -0
  71. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/curtain.py +0 -0
  72. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/evaporative_humidifier.py +0 -0
  73. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/fan.py +0 -0
  74. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/humidifier.py +0 -0
  75. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/keypad.py +0 -0
  76. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/light_strip.py +0 -0
  77. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/lock.py +0 -0
  78. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/meter.py +0 -0
  79. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/motion.py +0 -0
  80. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/plug.py +0 -0
  81. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/relay_switch.py +0 -0
  82. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/roller_shade.py +0 -0
  83. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/smart_thermostat_radiator.py +0 -0
  84. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/vacuum.py +0 -0
  85. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/discovery.py +0 -0
  86. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/enum.py +0 -0
  87. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/helpers.py +0 -0
  88. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/models.py +0 -0
  89. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/utils.py +0 -0
  90. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_air_purifier.py +0 -0
  91. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_base_cover.py +0 -0
  92. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_blind_tilt.py +0 -0
  93. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_bulb.py +0 -0
  94. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_ceiling_light.py +0 -0
  95. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_colormode_imports.py +0 -0
  96. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_curtain.py +0 -0
  97. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_encrypted_device.py +0 -0
  98. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_evaporative_humidifier.py +0 -0
  99. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_fan.py +0 -0
  100. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_helpers.py +0 -0
  101. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_hub2.py +0 -0
  102. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_hub3.py +0 -0
  103. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_lock.py +0 -0
  104. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_relay_switch.py +0 -0
  105. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_roller_shade.py +0 -0
  106. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_smart_thermostat_radiator.py +0 -0
  107. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_strip_light.py +0 -0
  108. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_utils.py +0 -0
  109. {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_vacuum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.74.0
3
+ Version: 0.76.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.74.0
3
+ Version: 0.76.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
@@ -18,6 +18,7 @@ switchbot/models.py
18
18
  switchbot/utils.py
19
19
  switchbot/adv_parsers/__init__.py
20
20
  switchbot/adv_parsers/air_purifier.py
21
+ switchbot/adv_parsers/art_frame.py
21
22
  switchbot/adv_parsers/blind_tilt.py
22
23
  switchbot/adv_parsers/bot.py
23
24
  switchbot/adv_parsers/bulb.py
@@ -31,6 +32,7 @@ switchbot/adv_parsers/hub3.py
31
32
  switchbot/adv_parsers/hubmini_matter.py
32
33
  switchbot/adv_parsers/humidifier.py
33
34
  switchbot/adv_parsers/keypad.py
35
+ switchbot/adv_parsers/keypad_vision.py
34
36
  switchbot/adv_parsers/leak.py
35
37
  switchbot/adv_parsers/light_strip.py
36
38
  switchbot/adv_parsers/lock.py
@@ -55,6 +57,7 @@ switchbot/const/lock.py
55
57
  switchbot/const/presence_sensor.py
56
58
  switchbot/devices/__init__.py
57
59
  switchbot/devices/air_purifier.py
60
+ switchbot/devices/art_frame.py
58
61
  switchbot/devices/base_cover.py
59
62
  switchbot/devices/base_light.py
60
63
  switchbot/devices/blind_tilt.py
@@ -68,6 +71,7 @@ switchbot/devices/evaporative_humidifier.py
68
71
  switchbot/devices/fan.py
69
72
  switchbot/devices/humidifier.py
70
73
  switchbot/devices/keypad.py
74
+ switchbot/devices/keypad_vision.py
71
75
  switchbot/devices/light_strip.py
72
76
  switchbot/devices/lock.py
73
77
  switchbot/devices/meter.py
@@ -79,6 +83,7 @@ switchbot/devices/smart_thermostat_radiator.py
79
83
  switchbot/devices/vacuum.py
80
84
  tests/test_adv_parser.py
81
85
  tests/test_air_purifier.py
86
+ tests/test_art_frame.py
82
87
  tests/test_base_cover.py
83
88
  tests/test_blind_tilt.py
84
89
  tests/test_bulb.py
@@ -92,6 +97,7 @@ tests/test_fan.py
92
97
  tests/test_helpers.py
93
98
  tests/test_hub2.py
94
99
  tests/test_hub3.py
100
+ tests/test_keypad_vision.py
95
101
  tests/test_lock.py
96
102
  tests/test_relay_switch.py
97
103
  tests/test_roller_shade.py
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="0.74.0",
23
+ version="0.76.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -29,6 +29,7 @@ from .const import (
29
29
  SwitchbotModel,
30
30
  )
31
31
  from .devices.air_purifier import SwitchbotAirPurifier
32
+ from .devices.art_frame import SwitchbotArtFrame
32
33
  from .devices.base_light import SwitchbotBaseLight
33
34
  from .devices.blind_tilt import SwitchbotBlindTilt
34
35
  from .devices.bot import Switchbot
@@ -44,6 +45,7 @@ from .devices.device import (
44
45
  from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
45
46
  from .devices.fan import SwitchbotFan
46
47
  from .devices.humidifier import SwitchbotHumidifier
48
+ from .devices.keypad_vision import SwitchbotKeypadVision
47
49
  from .devices.light_strip import (
48
50
  SwitchbotLightStrip,
49
51
  SwitchbotRgbicLight,
@@ -83,6 +85,7 @@ __all__ = [
83
85
  "SwitchbotAccountConnectionError",
84
86
  "SwitchbotAirPurifier",
85
87
  "SwitchbotApiError",
88
+ "SwitchbotArtFrame",
86
89
  "SwitchbotAuthenticationError",
87
90
  "SwitchbotBaseLight",
88
91
  "SwitchbotBlindTilt",
@@ -95,6 +98,7 @@ __all__ = [
95
98
  "SwitchbotFan",
96
99
  "SwitchbotGarageDoorOpener",
97
100
  "SwitchbotHumidifier",
101
+ "SwitchbotKeypadVision",
98
102
  "SwitchbotLightStrip",
99
103
  "SwitchbotLock",
100
104
  "SwitchbotModel",
@@ -12,6 +12,7 @@ from bleak.backends.device import BLEDevice
12
12
  from bleak.backends.scanner import AdvertisementData
13
13
 
14
14
  from .adv_parsers.air_purifier import process_air_purifier
15
+ from .adv_parsers.art_frame import process_art_frame
15
16
  from .adv_parsers.blind_tilt import process_woblindtilt
16
17
  from .adv_parsers.bot import process_wohand
17
18
  from .adv_parsers.bulb import process_color_bulb
@@ -25,6 +26,7 @@ from .adv_parsers.hub3 import process_hub3
25
26
  from .adv_parsers.hubmini_matter import process_hubmini_matter
26
27
  from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
27
28
  from .adv_parsers.keypad import process_wokeypad
29
+ from .adv_parsers.keypad_vision import process_keypad_vision, process_keypad_vision_pro
28
30
  from .adv_parsers.leak import process_leak
29
31
  from .adv_parsers.light_strip import process_light, process_rgbic_light, process_wostrip
30
32
  from .adv_parsers.lock import (
@@ -717,6 +719,42 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
717
719
  "func": process_presence_sensor,
718
720
  "manufacturer_id": 2409,
719
721
  },
722
+ b"\x00\x11>\x10": {
723
+ "modelName": SwitchbotModel.ART_FRAME,
724
+ "modelFriendlyName": "Art Frame",
725
+ "func": process_art_frame,
726
+ "manufacturer_id": 2409,
727
+ },
728
+ b"\x01\x11>\x10": {
729
+ "modelName": SwitchbotModel.ART_FRAME,
730
+ "modelFriendlyName": "Art Frame",
731
+ "func": process_art_frame,
732
+ "manufacturer_id": 2409,
733
+ },
734
+ b"\x00\x11\x03x": {
735
+ "modelName": SwitchbotModel.KEYPAD_VISION,
736
+ "modelFriendlyName": "Keypad Vision",
737
+ "func": process_keypad_vision,
738
+ "manufacturer_id": 2409,
739
+ },
740
+ b"\x01\x11\x03x": {
741
+ "modelName": SwitchbotModel.KEYPAD_VISION,
742
+ "modelFriendlyName": "Keypad Vision",
743
+ "func": process_keypad_vision,
744
+ "manufacturer_id": 2409,
745
+ },
746
+ b"\x00\x11Q\x98": {
747
+ "modelName": SwitchbotModel.KEYPAD_VISION_PRO,
748
+ "modelFriendlyName": "Keypad Vision Pro",
749
+ "func": process_keypad_vision_pro,
750
+ "manufacturer_id": 2409,
751
+ },
752
+ b"\x01\x11Q\x98": {
753
+ "modelName": SwitchbotModel.KEYPAD_VISION_PRO,
754
+ "modelFriendlyName": "Keypad Vision Pro",
755
+ "func": process_keypad_vision_pro,
756
+ "manufacturer_id": 2409,
757
+ },
720
758
  }
721
759
 
722
760
  _SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(
@@ -0,0 +1,35 @@
1
+ """Art Frame advertisement data parser."""
2
+
3
+ import logging
4
+
5
+ _LOGGER = logging.getLogger(__name__)
6
+
7
+
8
+ def process_art_frame(
9
+ data: bytes | None, mfr_data: bytes | None
10
+ ) -> dict[str, bool | int | str]:
11
+ """Process Art Frame data."""
12
+ if mfr_data is None:
13
+ return {}
14
+
15
+ _seq_num = mfr_data[6]
16
+ battery_charging = bool(mfr_data[7] & 0x80)
17
+ battery = mfr_data[7] & 0x7F
18
+ image_index = mfr_data[8]
19
+ display_size = (mfr_data[9] >> 4) & 0x0F
20
+ display_mode = (mfr_data[9] >> 3) & 0x01
21
+ last_network_status = (mfr_data[9] >> 2) & 0x01
22
+
23
+ result = {
24
+ "sequence_number": _seq_num,
25
+ "battery_charging": battery_charging,
26
+ "battery": battery,
27
+ "image_index": image_index,
28
+ "display_size": display_size,
29
+ "display_mode": display_mode,
30
+ "last_network_status": last_network_status,
31
+ }
32
+
33
+ _LOGGER.debug("Art Frame mfr data: %s, result: %s", mfr_data.hex(), result)
34
+
35
+ return result
@@ -0,0 +1,79 @@
1
+ """Keypad Vision (Pro) device data parsers."""
2
+
3
+ import logging
4
+
5
+ _LOGGER = logging.getLogger(__name__)
6
+
7
+
8
+ def process_common_mfr_data(mfr_data: bytes | None) -> dict[str, bool | int]:
9
+ """Process common Keypad Vision (Pro) manufacturer data."""
10
+ if mfr_data is None:
11
+ return {}
12
+
13
+ sequence_number = mfr_data[6]
14
+ battery_charging = bool(mfr_data[7] & 0b10000000)
15
+ battery = mfr_data[7] & 0b01111111
16
+ lockout_alarm = bool(mfr_data[8] & 0b00000001)
17
+ tamper_alarm = bool(mfr_data[8] & 0b00000010)
18
+ duress_alarm = bool(mfr_data[8] & 0b00000100)
19
+ low_temperature = bool(mfr_data[8] & 0b10000000)
20
+ high_temperature = bool(mfr_data[8] & 0b01000000)
21
+ doorbell = bool(mfr_data[12] & 0b00001000)
22
+
23
+ return {
24
+ "sequence_number": sequence_number,
25
+ "battery_charging": battery_charging,
26
+ "battery": battery,
27
+ "lockout_alarm": lockout_alarm,
28
+ "tamper_alarm": tamper_alarm,
29
+ "duress_alarm": duress_alarm,
30
+ "low_temperature": low_temperature,
31
+ "high_temperature": high_temperature,
32
+ "doorbell": doorbell,
33
+ }
34
+
35
+
36
+ def process_keypad_vision(
37
+ data: bytes | None, mfr_data: bytes | None
38
+ ) -> dict[str, bool | int | str]:
39
+ """Process Keypad Vision data."""
40
+ result = process_common_mfr_data(mfr_data)
41
+
42
+ if not result:
43
+ return {}
44
+
45
+ pir_triggered_level = mfr_data[13] & 0x03
46
+
47
+ result.update(
48
+ {
49
+ "pir_triggered_level": pir_triggered_level,
50
+ }
51
+ )
52
+
53
+ _LOGGER.debug("Keypad Vision mfr data: %s, result: %s", mfr_data.hex(), result)
54
+
55
+ return result
56
+
57
+
58
+ def process_keypad_vision_pro(
59
+ data: bytes | None, mfr_data: bytes | None
60
+ ) -> dict[str, bool | int | str]:
61
+ """Process Keypad Vision Pro data."""
62
+ result = process_common_mfr_data(mfr_data)
63
+
64
+ if not result:
65
+ return {}
66
+
67
+ radar_triggered_level = mfr_data[13] & 0x03
68
+ radar_triggered_distance = (mfr_data[13] >> 2) & 0x03
69
+
70
+ result.update(
71
+ {
72
+ "radar_triggered_level": radar_triggered_level,
73
+ "radar_triggered_distance": radar_triggered_distance,
74
+ }
75
+ )
76
+
77
+ _LOGGER.debug("Keypad Vision Pro mfr data: %s, result: %s", mfr_data.hex(), result)
78
+
79
+ return result
@@ -21,15 +21,17 @@ def process_presence_sensor(
21
21
  motion_detected = bool(mfr_data[7] & 0x40)
22
22
  battery_bits = (mfr_data[7] >> 2) & 0x03
23
23
  battery_range = BATTERY_LEVEL_MAP.get(battery_bits, "Unknown")
24
+ duration = (mfr_data[8] << 8) + mfr_data[9]
24
25
  trigger_flag = mfr_data[10]
25
26
  led_state = bool(mfr_data[11] & 0x80)
26
- light_level = mfr_data[11] & 0x0F
27
+ light_level = mfr_data[11] & 0x1F
27
28
 
28
29
  result = {
29
30
  "sequence_number": seq_number,
30
31
  "adaptive_state": adaptive_state,
31
32
  "motion_detected": motion_detected,
32
33
  "battery_range": battery_range,
34
+ "duration": duration,
33
35
  "trigger_flag": trigger_flag,
34
36
  "led_state": led_state,
35
37
  "lightLevel": light_level,
@@ -102,6 +102,9 @@ class SwitchbotModel(StrEnum):
102
102
  SMART_THERMOSTAT_RADIATOR = "Smart Thermostat Radiator"
103
103
  S20_VACUUM = "S20 Vacuum"
104
104
  PRESENCE_SENSOR = "Presence Sensor"
105
+ ART_FRAME = "Art Frame"
106
+ KEYPAD_VISION = "Keypad Vision"
107
+ KEYPAD_VISION_PRO = "Keypad Vision Pro"
105
108
 
106
109
 
107
110
  __all__ = [
@@ -0,0 +1,140 @@
1
+ """Device handler for the Art Frame."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from bleak.backends.device import BLEDevice
7
+
8
+ from ..const import SwitchbotModel
9
+ from .device import (
10
+ SwitchbotEncryptedDevice,
11
+ SwitchbotSequenceDevice,
12
+ update_after_operation,
13
+ )
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+
17
+ COMMAND_SET_IMAGE = "570F7A02{}"
18
+
19
+
20
+ class SwitchbotArtFrame(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
21
+ """Representation of a Switchbot Art Frame."""
22
+
23
+ def __init__(
24
+ self,
25
+ device: BLEDevice,
26
+ key_id: str,
27
+ encryption_key: str,
28
+ interface: int = 0,
29
+ model: SwitchbotModel = SwitchbotModel.ART_FRAME,
30
+ **kwargs: Any,
31
+ ) -> None:
32
+ super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
33
+ self.response_flag = True
34
+
35
+ @classmethod
36
+ async def verify_encryption_key(
37
+ cls,
38
+ device: BLEDevice,
39
+ key_id: str,
40
+ encryption_key: str,
41
+ model: SwitchbotModel = SwitchbotModel.ART_FRAME,
42
+ **kwargs: Any,
43
+ ) -> bool:
44
+ return await super().verify_encryption_key(
45
+ device, key_id, encryption_key, model, **kwargs
46
+ )
47
+
48
+ async def get_basic_info(self) -> dict[str, Any] | None:
49
+ """Get device basic settings."""
50
+ if not (_data := await self._get_basic_info()):
51
+ return None
52
+ _LOGGER.debug("basic info data: %s", _data.hex())
53
+
54
+ battery_charging = bool(_data[1] & 0x80)
55
+ battery = _data[1] & 0x7F
56
+ firmware = _data[2] / 10.0
57
+ hardware = _data[3]
58
+ display_size = (_data[4] >> 4) & 0x0F
59
+ display_mode = (_data[4] >> 3) & 0x01
60
+ last_network_status = (_data[4] >> 2) & 0x01
61
+ current_image_index = _data[5]
62
+ total_num_of_images = _data[6]
63
+ all_images_index = [_data[x] for x in range(7, 7 + total_num_of_images)]
64
+
65
+ basic_info = {
66
+ "battery_charging": battery_charging,
67
+ "battery": battery,
68
+ "firmware": firmware,
69
+ "hardware": hardware,
70
+ "display_size": display_size,
71
+ "display_mode": display_mode,
72
+ "last_network_status": last_network_status,
73
+ "current_image_index": current_image_index,
74
+ "total_num_of_images": total_num_of_images,
75
+ "all_images_index": all_images_index,
76
+ }
77
+ _LOGGER.debug("Art Frame %s basic info: %s", self._device.address, basic_info)
78
+ return basic_info
79
+
80
+ def _select_image_index(self, offset: int) -> int:
81
+ """Select the image index based on the current index and offset."""
82
+ current_index = self.get_current_image_index()
83
+ all_images_index = self.get_all_images_index()
84
+
85
+ if not all_images_index or len(all_images_index) <= 1:
86
+ raise RuntimeError("No images available to select from.")
87
+
88
+ new_position = (all_images_index.index(current_index) + offset) % len(
89
+ all_images_index
90
+ )
91
+ return all_images_index[new_position]
92
+
93
+ async def _get_current_image_index(self) -> None:
94
+ """Validate the current image index."""
95
+ if not await self.get_basic_info():
96
+ raise RuntimeError("Failed to retrieve basic info for current image index.")
97
+
98
+ @update_after_operation
99
+ async def next_image(self) -> bool:
100
+ """Display the next image."""
101
+ await self._get_current_image_index()
102
+ idx = self._select_image_index(1)
103
+ result = await self._send_command(COMMAND_SET_IMAGE.format(f"{idx:02X}"))
104
+ return self._check_command_result(result, 0, {1})
105
+
106
+ @update_after_operation
107
+ async def prev_image(self) -> bool:
108
+ """Display the previous image."""
109
+ await self._get_current_image_index()
110
+ idx = self._select_image_index(-1)
111
+ result = await self._send_command(COMMAND_SET_IMAGE.format(f"{idx:02X}"))
112
+ return self._check_command_result(result, 0, {1})
113
+
114
+ @update_after_operation
115
+ async def set_image(self, index: int) -> bool:
116
+ """Set the image by index."""
117
+ await self._get_current_image_index()
118
+ total_images = self.get_total_images()
119
+
120
+ if index < 0 or index >= total_images:
121
+ raise ValueError(
122
+ f"Image index {index} is out of range. Total images: {total_images}."
123
+ )
124
+
125
+ all_images_index = self.get_all_images_index()
126
+ img_index = all_images_index[index]
127
+ result = await self._send_command(COMMAND_SET_IMAGE.format(f"{img_index:02X}"))
128
+ return self._check_command_result(result, 0, {1})
129
+
130
+ def get_all_images_index(self) -> list[int] | None:
131
+ """Return cached list of all image indexes."""
132
+ return self._get_adv_value("all_images_index")
133
+
134
+ def get_current_image_index(self) -> int | None:
135
+ """Return cached current image index."""
136
+ return self._get_adv_value("current_image_index")
137
+
138
+ def get_total_images(self) -> int | None:
139
+ """Return cached total number of images."""
140
+ return self._get_adv_value("total_num_of_images")
@@ -43,19 +43,6 @@ class SwitchbotBaseCover(SwitchbotDevice):
43
43
  self._is_opening: bool = False
44
44
  self._is_closing: bool = False
45
45
 
46
- async def _send_multiple_commands(self, keys: list[str]) -> bool:
47
- """
48
- Send multiple commands to device.
49
-
50
- Since we current have no way to tell which command the device
51
- needs we send both.
52
- """
53
- final_result = False
54
- for key in keys:
55
- result = await self._send_command(key)
56
- final_result |= self._check_command_result(result, 0, {1})
57
- return final_result
58
-
59
46
  @update_after_operation
60
47
  async def stop(self) -> bool:
61
48
  """Send stop command to device."""
@@ -119,19 +119,6 @@ class SwitchbotBaseLight(SwitchbotDevice):
119
119
  self._override_state({"effect": effect})
120
120
  return result
121
121
 
122
- async def _send_multiple_commands(self, keys: list[str]) -> bool:
123
- """
124
- Send multiple commands to device.
125
-
126
- Since we current have no way to tell which command the device
127
- needs we send both.
128
- """
129
- final_result = False
130
- for key in keys:
131
- result = await self._send_command(key)
132
- final_result |= self._check_command_result(result, 0, {1})
133
- return final_result
134
-
135
122
  async def _get_multi_commands_results(
136
123
  self, commands: list[str]
137
124
  ) -> tuple[bytes, bytes] | None:
@@ -104,6 +104,7 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
104
104
  "W1104000": SwitchbotModel.PLUG_MINI_EU,
105
105
  "W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
106
106
  "W1111000": SwitchbotModel.CLIMATE_PANEL,
107
+ "W1130000": SwitchbotModel.ART_FRAME,
107
108
  }
108
109
 
109
110
  REQ_HEADER = "570f"
@@ -224,7 +225,6 @@ class SwitchbotBaseDevice:
224
225
  self._write_char: BleakGATTCharacteristic | None = None
225
226
  self._disconnect_timer: asyncio.TimerHandle | None = None
226
227
  self._expected_disconnect = False
227
- self.loop = asyncio.get_event_loop()
228
228
  self._callbacks: list[Callable[[], None]] = []
229
229
  self._notify_future: asyncio.Future[bytearray] | None = None
230
230
  self._last_full_update: float = -PASSIVE_POLL_INTERVAL
@@ -546,7 +546,7 @@ class SwitchbotBaseDevice:
546
546
  """Reset disconnect timer."""
547
547
  self._cancel_disconnect_timer()
548
548
  self._expected_disconnect = False
549
- self._disconnect_timer = self.loop.call_later(
549
+ self._disconnect_timer = asyncio.get_running_loop().call_later(
550
550
  DISCONNECT_DELAY, self._disconnect_from_timer
551
551
  )
552
552
 
@@ -679,15 +679,16 @@ class SwitchbotBaseDevice:
679
679
  assert self._client is not None
680
680
  assert self._read_char is not None
681
681
  assert self._write_char is not None
682
- self._notify_future = self.loop.create_future()
682
+ loop = asyncio.get_running_loop()
683
+ self._notify_future = loop.create_future()
683
684
  client = self._client
684
685
 
685
686
  _LOGGER.debug("%s: Sending command: %s", self.name, key)
686
687
  await client.write_gatt_char(self._write_char, command, False)
687
688
 
688
689
  timeout = 5
689
- timeout_handle = self.loop.call_at(
690
- self.loop.time() + timeout, _handle_timeout, self._notify_future
690
+ timeout_handle = loop.call_at(
691
+ loop.time() + timeout, _handle_timeout, self._notify_future
691
692
  )
692
693
  timeout_expired = False
693
694
  try:
@@ -930,6 +931,32 @@ class SwitchbotDevice(SwitchbotBaseDevice):
930
931
  super().update_from_advertisement(advertisement)
931
932
  self._set_advertisement_data(advertisement)
932
933
 
934
+ async def _send_multiple_commands(self, keys: list[str]) -> bool:
935
+ """
936
+ Send multiple commands to device.
937
+
938
+ Returns True if any command succeeds. Used when we don't know
939
+ which command the device needs, so we send multiple and consider
940
+ it successful if any one works.
941
+ """
942
+ final_result = False
943
+ for key in keys:
944
+ result = await self._send_command(key)
945
+ final_result |= self._check_command_result(result, 0, {1})
946
+ return final_result
947
+
948
+ async def _send_command_sequence(self, keys: list[str]) -> bool:
949
+ """
950
+ Send a sequence of commands to device where all must succeed.
951
+
952
+ Returns True only if all commands succeed.
953
+ """
954
+ for key in keys:
955
+ result = await self._send_command(key)
956
+ if not self._check_command_result(result, 0, {1}):
957
+ return False
958
+ return True
959
+
933
960
 
934
961
  class SwitchbotEncryptedDevice(SwitchbotDevice):
935
962
  """A Switchbot device that uses encryption."""