PySwitchbot 1.0.0__tar.gz → 2.0.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 (115) hide show
  1. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/PKG-INFO +1 -1
  2. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/pyproject.toml +0 -1
  4. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/setup.py +1 -1
  5. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/__init__.py +2 -0
  6. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parser.py +52 -16
  7. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/__init__.py +9 -3
  8. pyswitchbot-2.0.0/switchbot/devices/air_purifier.py +311 -0
  9. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/base_light.py +26 -8
  10. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/device.py +23 -4
  11. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/lock.py +20 -2
  12. pyswitchbot-2.0.0/switchbot/enum.py +7 -0
  13. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_adv_parser.py +177 -15
  14. pyswitchbot-2.0.0/tests/test_air_purifier.py +484 -0
  15. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_bulb.py +8 -18
  16. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_ceiling_light.py +8 -18
  17. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_lock.py +127 -3
  18. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_strip_light.py +8 -16
  19. pyswitchbot-1.0.0/switchbot/devices/air_purifier.py +0 -131
  20. pyswitchbot-1.0.0/switchbot/enum.py +0 -20
  21. pyswitchbot-1.0.0/tests/test_air_purifier.py +0 -231
  22. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/LICENSE +0 -0
  23. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/MANIFEST.in +0 -0
  24. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/SOURCES.txt +0 -0
  25. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  26. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/requires.txt +0 -0
  27. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  28. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/README.md +0 -0
  29. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/setup.cfg +0 -0
  30. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/__init__.py +0 -0
  31. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  32. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/art_frame.py +0 -0
  33. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  34. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/bot.py +0 -0
  35. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/bulb.py +0 -0
  36. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  37. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/climate_panel.py +0 -0
  38. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/contact.py +0 -0
  39. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/curtain.py +0 -0
  40. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/fan.py +0 -0
  41. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/hub2.py +0 -0
  42. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/hub3.py +0 -0
  43. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  44. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/humidifier.py +0 -0
  45. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/keypad.py +0 -0
  46. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/keypad_vision.py +0 -0
  47. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/leak.py +0 -0
  48. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/light_strip.py +0 -0
  49. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/lock.py +0 -0
  50. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/meter.py +0 -0
  51. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/motion.py +0 -0
  52. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/plug.py +0 -0
  53. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/presence_sensor.py +0 -0
  54. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  55. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/remote.py +0 -0
  56. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  57. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
  58. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/vacuum.py +0 -0
  59. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/api_config.py +0 -0
  60. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/air_purifier.py +0 -0
  61. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/climate.py +0 -0
  62. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/evaporative_humidifier.py +0 -0
  63. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/fan.py +0 -0
  64. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/hub2.py +0 -0
  65. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/hub3.py +0 -0
  66. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/light.py +0 -0
  67. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/lock.py +0 -0
  68. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/const/presence_sensor.py +0 -0
  69. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/__init__.py +0 -0
  70. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/art_frame.py +0 -0
  71. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/base_cover.py +0 -0
  72. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/blind_tilt.py +0 -0
  73. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/bot.py +0 -0
  74. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/bulb.py +0 -0
  75. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/ceiling_light.py +0 -0
  76. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/contact.py +0 -0
  77. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/curtain.py +0 -0
  78. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/evaporative_humidifier.py +0 -0
  79. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/fan.py +0 -0
  80. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/humidifier.py +0 -0
  81. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/keypad.py +0 -0
  82. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/keypad_vision.py +0 -0
  83. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/light_strip.py +0 -0
  84. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/meter.py +0 -0
  85. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/meter_pro.py +0 -0
  86. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/motion.py +0 -0
  87. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/plug.py +0 -0
  88. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/relay_switch.py +0 -0
  89. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/roller_shade.py +0 -0
  90. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/smart_thermostat_radiator.py +0 -0
  91. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/devices/vacuum.py +0 -0
  92. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/discovery.py +0 -0
  93. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/helpers.py +0 -0
  94. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/models.py +0 -0
  95. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/switchbot/utils.py +0 -0
  96. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_art_frame.py +0 -0
  97. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_base_cover.py +0 -0
  98. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_blind_tilt.py +0 -0
  99. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_colormode_imports.py +0 -0
  100. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_curtain.py +0 -0
  101. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_device.py +0 -0
  102. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_discovery_callback.py +0 -0
  103. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_encrypted_device.py +0 -0
  104. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_evaporative_humidifier.py +0 -0
  105. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_fan.py +0 -0
  106. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_helpers.py +0 -0
  107. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_hub2.py +0 -0
  108. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_hub3.py +0 -0
  109. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_keypad_vision.py +0 -0
  110. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_meter_pro.py +0 -0
  111. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_relay_switch.py +0 -0
  112. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_roller_shade.py +0 -0
  113. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_smart_thermostat_radiator.py +0 -0
  114. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_utils.py +0 -0
  115. {pyswitchbot-1.0.0 → pyswitchbot-2.0.0}/tests/test_vacuum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 1.0.0
3
+ Version: 2.0.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: 1.0.0
3
+ Version: 2.0.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,7 +25,6 @@ ignore = [
25
25
  "S110", # `try`-`except`-`pass` detected, consider logging the exception
26
26
  "D106", # Missing docstring in public nested class
27
27
  "UP007", # typer needs Optional syntax
28
- "UP038", # Use `X | Y` in `isinstance` is slower
29
28
  "S603", # check for execution of untrusted input
30
29
  "S105", # possible hard coded creds
31
30
  "TID252", # not for this lib
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="1.0.0",
23
+ version="2.0.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -11,6 +11,7 @@ from bleak_retry_connector import (
11
11
  from .adv_parser import SwitchbotSupportedType, parse_advertisement_data
12
12
  from .const import (
13
13
  AirPurifierMode,
14
+ AirQualityLevel,
14
15
  BulbColorMode,
15
16
  CeilingLightColorMode,
16
17
  ClimateAction,
@@ -67,6 +68,7 @@ from .models import SwitchBotAdvertisement
67
68
 
68
69
  __all__ = [
69
70
  "AirPurifierMode",
71
+ "AirQualityLevel",
70
72
  "BulbColorMode",
71
73
  "CeilingLightColorMode",
72
74
  "ClimateAction",
@@ -492,50 +492,50 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
492
492
  "manufacturer_id": 2409,
493
493
  },
494
494
  "*": {
495
- "modelName": SwitchbotModel.AIR_PURIFIER,
496
- "modelFriendlyName": "Air Purifier",
495
+ "modelName": SwitchbotModel.AIR_PURIFIER_US,
496
+ "modelFriendlyName": "Air Purifier US",
497
497
  "func": process_air_purifier,
498
498
  "manufacturer_id": 2409,
499
499
  },
500
500
  b"\x0a": {
501
- "modelName": SwitchbotModel.AIR_PURIFIER,
502
- "modelFriendlyName": "Air Purifier",
501
+ "modelName": SwitchbotModel.AIR_PURIFIER_US,
502
+ "modelFriendlyName": "Air Purifier US",
503
503
  "func": process_air_purifier,
504
504
  "manufacturer_id": 2409,
505
505
  },
506
506
  "+": {
507
- "modelName": SwitchbotModel.AIR_PURIFIER,
508
- "modelFriendlyName": "Air Purifier",
507
+ "modelName": SwitchbotModel.AIR_PURIFIER_JP,
508
+ "modelFriendlyName": "Air Purifier JP",
509
509
  "func": process_air_purifier,
510
510
  "manufacturer_id": 2409,
511
511
  },
512
512
  b"\x0b": {
513
- "modelName": SwitchbotModel.AIR_PURIFIER,
514
- "modelFriendlyName": "Air Purifier",
513
+ "modelName": SwitchbotModel.AIR_PURIFIER_JP,
514
+ "modelFriendlyName": "Air Purifier JP",
515
515
  "func": process_air_purifier,
516
516
  "manufacturer_id": 2409,
517
517
  },
518
518
  "7": {
519
- "modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
520
- "modelFriendlyName": "Air Purifier Table",
519
+ "modelName": SwitchbotModel.AIR_PURIFIER_TABLE_US,
520
+ "modelFriendlyName": "Air Purifier Table US",
521
521
  "func": process_air_purifier,
522
522
  "manufacturer_id": 2409,
523
523
  },
524
524
  b"\x17": {
525
- "modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
526
- "modelFriendlyName": "Air Purifier Table",
525
+ "modelName": SwitchbotModel.AIR_PURIFIER_TABLE_US,
526
+ "modelFriendlyName": "Air Purifier Table US",
527
527
  "func": process_air_purifier,
528
528
  "manufacturer_id": 2409,
529
529
  },
530
530
  "8": {
531
- "modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
532
- "modelFriendlyName": "Air Purifier Table",
531
+ "modelName": SwitchbotModel.AIR_PURIFIER_TABLE_JP,
532
+ "modelFriendlyName": "Air Purifier Table JP",
533
533
  "func": process_air_purifier,
534
534
  "manufacturer_id": 2409,
535
535
  },
536
536
  b"\x18": {
537
- "modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
538
- "modelFriendlyName": "Air Purifier Table",
537
+ "modelName": SwitchbotModel.AIR_PURIFIER_TABLE_JP,
538
+ "modelFriendlyName": "Air Purifier Table JP",
539
539
  "func": process_air_purifier,
540
540
  "manufacturer_id": 2409,
541
541
  },
@@ -755,6 +755,42 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
755
755
  "func": process_keypad_vision_pro,
756
756
  "manufacturer_id": 2409,
757
757
  },
758
+ b"\x00\x11\x69\x09": {
759
+ "modelName": SwitchbotModel.LOCK_VISION_PRO,
760
+ "modelFriendlyName": "Lock Vision Pro",
761
+ "func": process_lock2,
762
+ "manufacturer_id": 2409,
763
+ },
764
+ b"\x01\x11\x69\x09": {
765
+ "modelName": SwitchbotModel.LOCK_VISION_PRO,
766
+ "modelFriendlyName": "Lock Vision Pro",
767
+ "func": process_lock2,
768
+ "manufacturer_id": 2409,
769
+ },
770
+ b"\x00\x11\x69\x08": {
771
+ "modelName": SwitchbotModel.LOCK_VISION,
772
+ "modelFriendlyName": "Lock Vision",
773
+ "func": process_locklite,
774
+ "manufacturer_id": 2409,
775
+ },
776
+ b"\x01\x11\x69\x08": {
777
+ "modelName": SwitchbotModel.LOCK_VISION,
778
+ "modelFriendlyName": "Lock Vision",
779
+ "func": process_locklite,
780
+ "manufacturer_id": 2409,
781
+ },
782
+ b"\x00\x10\xff\x90": {
783
+ "modelName": SwitchbotModel.LOCK_PRO_WIFI,
784
+ "modelFriendlyName": "Lock Pro Wifi",
785
+ "func": process_wolock_pro,
786
+ "manufacturer_id": 2409,
787
+ },
788
+ b"\x01\x10\xff\x90": {
789
+ "modelName": SwitchbotModel.LOCK_PRO_WIFI,
790
+ "modelFriendlyName": "Lock Pro Wifi",
791
+ "func": process_wolock_pro,
792
+ "manufacturer_id": 2409,
793
+ },
758
794
  }
759
795
 
760
796
  _SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from ..enum import StrEnum
6
- from .air_purifier import AirPurifierMode
6
+ from .air_purifier import AirPurifierMode, AirQualityLevel
7
7
  from .climate import ClimateAction, ClimateMode, SmartThermostatRadiatorMode
8
8
  from .evaporative_humidifier import (
9
9
  HumidifierAction,
@@ -85,8 +85,10 @@ class SwitchbotModel(StrEnum):
85
85
  K10_VACUUM = "K10+ Vacuum"
86
86
  K10_PRO_VACUUM = "K10+ Pro Vacuum"
87
87
  K10_PRO_COMBO_VACUUM = "K10+ Pro Combo Vacuum"
88
- AIR_PURIFIER = "Air Purifier"
89
- AIR_PURIFIER_TABLE = "Air Purifier Table"
88
+ AIR_PURIFIER_US = "Air Purifier US"
89
+ AIR_PURIFIER_JP = "Air Purifier JP"
90
+ AIR_PURIFIER_TABLE_US = "Air Purifier Table US"
91
+ AIR_PURIFIER_TABLE_JP = "Air Purifier Table JP"
90
92
  HUB3 = "Hub3"
91
93
  LOCK_ULTRA = "Lock Ultra"
92
94
  LOCK_LITE = "Lock Lite"
@@ -105,6 +107,9 @@ class SwitchbotModel(StrEnum):
105
107
  ART_FRAME = "Art Frame"
106
108
  KEYPAD_VISION = "Keypad Vision"
107
109
  KEYPAD_VISION_PRO = "Keypad Vision Pro"
110
+ LOCK_VISION_PRO = "Lock Vision Pro"
111
+ LOCK_VISION = "Lock Vision"
112
+ LOCK_PRO_WIFI = "Lock Pro Wifi"
108
113
 
109
114
 
110
115
  __all__ = [
@@ -112,6 +117,7 @@ __all__ = [
112
117
  "DEFAULT_RETRY_TIMEOUT",
113
118
  "DEFAULT_SCAN_TIMEOUT",
114
119
  "AirPurifierMode",
120
+ "AirQualityLevel",
115
121
  "BulbColorMode",
116
122
  "CeilingLightColorMode",
117
123
  "ClimateAction",
@@ -0,0 +1,311 @@
1
+ """Library to handle connection with Switchbot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, ClassVar
7
+
8
+ from bleak.backends.device import BLEDevice
9
+
10
+ from ..adv_parsers.air_purifier import get_air_purifier_mode
11
+ from ..const import SwitchbotModel
12
+ from ..const.air_purifier import AirPurifierMode, AirQualityLevel
13
+ from ..const.light import ColorMode
14
+ from ..helpers import _UNPACK_UINT16_BE
15
+ from .base_light import SwitchbotSequenceBaseLight
16
+ from .device import (
17
+ SwitchbotEncryptedDevice,
18
+ SwitchbotOperationError,
19
+ update_after_operation,
20
+ )
21
+
22
+ _LOGGER = logging.getLogger(__name__)
23
+
24
+
25
+ COMMAND_HEAD = "570f4c"
26
+ COMMAND_SET_MODE = {
27
+ AirPurifierMode.LEVEL_1.name.lower(): f"{COMMAND_HEAD}01010100",
28
+ AirPurifierMode.LEVEL_2.name.lower(): f"{COMMAND_HEAD}01010132",
29
+ AirPurifierMode.LEVEL_3.name.lower(): f"{COMMAND_HEAD}01010164",
30
+ AirPurifierMode.AUTO.name.lower(): f"{COMMAND_HEAD}01010200",
31
+ AirPurifierMode.SLEEP.name.lower(): f"{COMMAND_HEAD}01010300",
32
+ AirPurifierMode.PET.name.lower(): f"{COMMAND_HEAD}01010400",
33
+ }
34
+ DEVICE_GET_BASIC_SETTINGS_KEY = "570f4d81"
35
+ COMMAND_SET_PERCENTAGE = f"{COMMAND_HEAD}02{{percentage:02x}}"
36
+ READ_LED_SETTINGS_COMMAND = "570f4d05"
37
+ READ_LED_STATUS_COMMAND = "570f4d07"
38
+
39
+
40
+ class SwitchbotAirPurifier(SwitchbotSequenceBaseLight, SwitchbotEncryptedDevice):
41
+ """Representation of a Switchbot Air Purifier."""
42
+
43
+ _turn_on_command = f"{COMMAND_HEAD}010100"
44
+ _turn_off_command = f"{COMMAND_HEAD}010000"
45
+ _open_child_lock_command = f"{COMMAND_HEAD}0301"
46
+ _close_child_lock_command = f"{COMMAND_HEAD}0300"
47
+ _open_wireless_charging_command = f"{COMMAND_HEAD}0d01"
48
+ _close_wireless_charging_command = f"{COMMAND_HEAD}0d00"
49
+ _open_light_sensitive_switch_command = f"{COMMAND_HEAD}0702"
50
+ _turn_led_on_command = f"{COMMAND_HEAD}0701"
51
+ _turn_led_off_command = f"{COMMAND_HEAD}0700"
52
+ _set_rgb_command = _set_brightness_command = f"{COMMAND_HEAD}0501{{}}"
53
+ _get_basic_info_command = [
54
+ DEVICE_GET_BASIC_SETTINGS_KEY,
55
+ READ_LED_SETTINGS_COMMAND,
56
+ READ_LED_STATUS_COMMAND,
57
+ ]
58
+
59
+ _PM25_MODELS: ClassVar[frozenset[SwitchbotModel]] = frozenset(
60
+ {
61
+ SwitchbotModel.AIR_PURIFIER_US,
62
+ SwitchbotModel.AIR_PURIFIER_TABLE_US,
63
+ }
64
+ )
65
+
66
+ _LEVEL_MODES: ClassVar[frozenset[str]] = frozenset(
67
+ {
68
+ AirPurifierMode.LEVEL_1.name.lower(),
69
+ AirPurifierMode.LEVEL_2.name.lower(),
70
+ AirPurifierMode.LEVEL_3.name.lower(),
71
+ }
72
+ )
73
+
74
+ _WIRELESS_MODELS: ClassVar[frozenset[SwitchbotModel]] = frozenset(
75
+ {
76
+ SwitchbotModel.AIR_PURIFIER_TABLE_US,
77
+ SwitchbotModel.AIR_PURIFIER_TABLE_JP,
78
+ }
79
+ )
80
+
81
+ def __init__(
82
+ self,
83
+ device: BLEDevice,
84
+ key_id: str,
85
+ encryption_key: str,
86
+ interface: int = 0,
87
+ model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER_US,
88
+ **kwargs: Any,
89
+ ) -> None:
90
+ super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
91
+
92
+ @classmethod
93
+ async def verify_encryption_key(
94
+ cls,
95
+ device: BLEDevice,
96
+ key_id: str,
97
+ encryption_key: str,
98
+ model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER_US,
99
+ **kwargs: Any,
100
+ ) -> bool:
101
+ return await super().verify_encryption_key(
102
+ device, key_id, encryption_key, model, **kwargs
103
+ )
104
+
105
+ @property
106
+ def color_modes(self) -> set[ColorMode]:
107
+ """Return the supported color modes."""
108
+ return {ColorMode.RGB}
109
+
110
+ @property
111
+ def color_mode(self) -> ColorMode:
112
+ """Return the current color mode."""
113
+ return ColorMode.RGB
114
+
115
+ async def get_basic_info(self) -> dict[str, Any] | None:
116
+ """Get device basic settings."""
117
+ if not (
118
+ res := await self._get_basic_info_by_multi_commands(
119
+ self._get_basic_info_command
120
+ )
121
+ ):
122
+ return None
123
+
124
+ _data, led_settings, led_status = res[0], res[1], res[2]
125
+
126
+ _LOGGER.debug(
127
+ "%s %s basic info %s", self._model, self._device.address, _data.hex()
128
+ )
129
+ _LOGGER.debug(
130
+ "%s %s led settings %s",
131
+ self._model,
132
+ self._device.address,
133
+ led_settings.hex(),
134
+ )
135
+ _LOGGER.debug(
136
+ "%s %s led_status %s", self._model, self._device.address, led_status.hex()
137
+ )
138
+ isOn = bool(_data[2] & 0b10000000)
139
+ wireless_charging = bool(_data[2] & 0b01000000)
140
+ version_info = (_data[2] & 0b00110000) >> 4
141
+ _mode = _data[2] & 0b00000111
142
+ isAqiValid = bool(_data[3] & 0b00000100)
143
+ child_lock = bool(_data[3] & 0b00000010)
144
+ _aqi_level = (_data[4] & 0b00000110) >> 1
145
+ aqi_level = AirQualityLevel(_aqi_level).name.lower()
146
+ speed = _data[6] & 0b01111111
147
+ pm25 = _UNPACK_UINT16_BE(_data, 12)[0] & 0xFFF
148
+ firmware = _data[15] / 10.0
149
+ mode = get_air_purifier_mode(_mode, speed)
150
+ self._state["r"] = led_settings[2]
151
+ self._state["g"] = led_settings[3]
152
+ self._state["b"] = led_settings[4]
153
+ brightness = led_settings[5]
154
+ light_sensitive = bool(led_status[1] & 0x02)
155
+
156
+ data = {
157
+ "isOn": isOn,
158
+ "version_info": version_info,
159
+ "mode": mode,
160
+ "isAqiValid": isAqiValid,
161
+ "child_lock": child_lock,
162
+ "aqi_level": aqi_level,
163
+ "speed": speed,
164
+ "firmware": firmware,
165
+ "brightness": brightness,
166
+ "light_sensitive": light_sensitive,
167
+ }
168
+ if self._model in self._WIRELESS_MODELS:
169
+ data["wireless_charging"] = wireless_charging
170
+
171
+ if self._model in self._PM25_MODELS:
172
+ return data | {"pm25": pm25}
173
+ return data
174
+
175
+ @update_after_operation
176
+ async def set_preset_mode(self, preset_mode: str) -> bool:
177
+ """Send command to set air purifier preset_mode."""
178
+ result = await self._send_command(COMMAND_SET_MODE[preset_mode])
179
+ return self._check_command_result(result, 0, {1})
180
+
181
+ @update_after_operation
182
+ async def set_percentage(self, percentage: int) -> bool:
183
+ """Set percentage."""
184
+ if not 0 <= percentage <= 100:
185
+ raise ValueError("Percentage must be between 0 and 100")
186
+ self._validate_current_mode()
187
+
188
+ result = await self._send_command(
189
+ COMMAND_SET_PERCENTAGE.format(percentage=percentage)
190
+ )
191
+ return self._check_command_result(result, 0, {1})
192
+
193
+ def _validate_current_mode(self) -> None:
194
+ """Validate current mode for setting percentage."""
195
+ current_mode = self.get_current_mode()
196
+ if current_mode not in self._LEVEL_MODES:
197
+ raise ValueError("Percentage can only be set in LEVEL modes.")
198
+
199
+ @update_after_operation
200
+ async def set_brightness(self, brightness: int) -> bool:
201
+ """Set brightness."""
202
+ self._validate_brightness(brightness)
203
+ r, g, b = (
204
+ self._state.get("r", 0),
205
+ self._state.get("g", 0),
206
+ self._state.get("b", 0),
207
+ )
208
+ hex_data = f"{r:02X}{g:02X}{b:02X}{brightness:02X}"
209
+ result = await self._send_command(self._set_brightness_command.format(hex_data))
210
+ return self._check_command_result(result, 0, {1})
211
+
212
+ @update_after_operation
213
+ async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
214
+ """
215
+ Set rgb.
216
+
217
+ Note: byte order is reversed from base class (RGB+brightness
218
+ instead of brightness+RGB).
219
+ """
220
+ self._validate_brightness(brightness)
221
+ self._validate_rgb(r, g, b)
222
+ hex_data = f"{r:02X}{g:02X}{b:02X}{brightness:02X}"
223
+ result = await self._send_command(self._set_rgb_command.format(hex_data))
224
+ return self._check_command_result(result, 0, {1})
225
+
226
+ @update_after_operation
227
+ async def turn_led_on(self) -> bool:
228
+ """Turn on LED."""
229
+ result = await self._send_command(self._turn_led_on_command)
230
+ return self._check_command_result(result, 0, {1})
231
+
232
+ @update_after_operation
233
+ async def turn_led_off(self) -> bool:
234
+ """Turn off LED."""
235
+ result = await self._send_command(self._turn_led_off_command)
236
+ return self._check_command_result(result, 0, {1})
237
+
238
+ @update_after_operation
239
+ async def open_light_sensitive_switch(self) -> bool:
240
+ """
241
+ Open the light sensitive switch.
242
+
243
+ This will allow the LED to automatically adjust brightness based on ambient light.
244
+ The LED will turn on in dark environments and turn off in bright environments.
245
+ """
246
+ result = await self._send_command(self._open_light_sensitive_switch_command)
247
+ return self._check_command_result(result, 0, {1})
248
+
249
+ @update_after_operation
250
+ async def close_light_sensitive_switch(self) -> bool:
251
+ """
252
+ Close the light sensitive switch.
253
+
254
+ Since the current protocol does not support obtaining the LED status,
255
+ sending an on or off command will turn off the light sensitive switch.
256
+ """
257
+ result = await self._send_command(self._turn_led_on_command)
258
+ return self._check_command_result(result, 0, {1})
259
+
260
+ def _check_wireless_charging_supported(self) -> None:
261
+ if self._model not in self._WIRELESS_MODELS:
262
+ raise SwitchbotOperationError(
263
+ "Wireless charging is only available on table versions"
264
+ f" (current model={self._model})"
265
+ )
266
+
267
+ @update_after_operation
268
+ async def open_wireless_charging(self) -> bool:
269
+ """Enable the wireless charging pad (table models only)."""
270
+ self._check_wireless_charging_supported()
271
+ result = await self._send_command(self._open_wireless_charging_command)
272
+ return self._check_command_result(result, 0, {1})
273
+
274
+ @update_after_operation
275
+ async def close_wireless_charging(self) -> bool:
276
+ """Disable the wireless charging pad (table models only)."""
277
+ self._check_wireless_charging_supported()
278
+ result = await self._send_command(self._close_wireless_charging_command)
279
+ return self._check_command_result(result, 0, {1})
280
+
281
+ def is_on(self) -> bool | None:
282
+ """Return air purifier state from cache."""
283
+ return self._get_adv_value("isOn")
284
+
285
+ def get_current_aqi_level(self) -> Any:
286
+ """Return cached aqi level."""
287
+ return self._get_adv_value("aqi_level")
288
+
289
+ def get_current_pm25(self) -> Any:
290
+ """Return cached pm25."""
291
+ return self._get_adv_value("pm25")
292
+
293
+ def get_current_mode(self) -> Any:
294
+ """Return cached mode."""
295
+ return self._get_adv_value("mode")
296
+
297
+ def is_child_lock_on(self) -> bool | None:
298
+ """Return child lock state from cache."""
299
+ return self._get_adv_value("child_lock")
300
+
301
+ def is_wireless_charging_on(self) -> bool | None:
302
+ """Return wireless charging state from cache."""
303
+ return self._get_adv_value("wireless_charging")
304
+
305
+ def get_current_percentage(self) -> int | None:
306
+ """Return cached percentage."""
307
+ return self._get_adv_value("speed")
308
+
309
+ def is_light_sensitive_on(self) -> bool | None:
310
+ """Return light sensitive state from cache."""
311
+ return self._get_adv_value("light_sensitive")
@@ -75,10 +75,29 @@ class SwitchbotBaseLight(SwitchbotDevice):
75
75
  """Return the current effect."""
76
76
  return self._get_adv_value("effect")
77
77
 
78
+ @staticmethod
79
+ def _validate_brightness(brightness: int) -> None:
80
+ if not 0 <= brightness <= 100:
81
+ raise ValueError("Brightness must be between 0 and 100")
82
+
83
+ @staticmethod
84
+ def _validate_rgb(r: int, g: int, b: int) -> None:
85
+ if not 0 <= r <= 255:
86
+ raise ValueError("r must be between 0 and 255")
87
+ if not 0 <= g <= 255:
88
+ raise ValueError("g must be between 0 and 255")
89
+ if not 0 <= b <= 255:
90
+ raise ValueError("b must be between 0 and 255")
91
+
92
+ @staticmethod
93
+ def _validate_color_temp(color_temp: int) -> None:
94
+ if not 2700 <= color_temp <= 6500:
95
+ raise ValueError("Color Temp must be between 2700 and 6500")
96
+
78
97
  @update_after_operation
79
98
  async def set_brightness(self, brightness: int) -> bool:
80
99
  """Set brightness."""
81
- assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
100
+ self._validate_brightness(brightness)
82
101
  hex_brightness = f"{brightness:02X}"
83
102
  self._check_function_support(self._set_brightness_command)
84
103
  result = await self._send_command(
@@ -89,8 +108,8 @@ class SwitchbotBaseLight(SwitchbotDevice):
89
108
  @update_after_operation
90
109
  async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
91
110
  """Set color temp."""
92
- assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
93
- assert 2700 <= color_temp <= 6500, "Color Temp must be between 2700 and 6500"
111
+ self._validate_brightness(brightness)
112
+ self._validate_color_temp(color_temp)
94
113
  hex_data = f"{brightness:02X}{color_temp:04X}"
95
114
  self._check_function_support(self._set_color_temp_command)
96
115
  result = await self._send_command(self._set_color_temp_command.format(hex_data))
@@ -99,10 +118,8 @@ class SwitchbotBaseLight(SwitchbotDevice):
99
118
  @update_after_operation
100
119
  async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
101
120
  """Set rgb."""
102
- assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
103
- assert 0 <= r <= 255, "r must be between 0 and 255"
104
- assert 0 <= g <= 255, "g must be between 0 and 255"
105
- assert 0 <= b <= 255, "b must be between 0 and 255"
121
+ self._validate_brightness(brightness)
122
+ self._validate_rgb(r, g, b)
106
123
  self._check_function_support(self._set_rgb_command)
107
124
  hex_data = f"{brightness:02X}{r:02X}{g:02X}{b:02X}"
108
125
  result = await self._send_command(self._set_rgb_command.format(hex_data))
@@ -141,7 +158,8 @@ class SwitchbotBaseLight(SwitchbotDevice):
141
158
  """Get device basic settings by sending multiple commands."""
142
159
  results = []
143
160
  for command in commands:
144
- if not (result := await self._get_basic_info(command)):
161
+ result = await self._send_command(command)
162
+ if not self._check_command_result(result, 0, {1}):
145
163
  return None
146
164
  results.append(result)
147
165
  return results
@@ -78,10 +78,10 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
78
78
  "WoFan2": SwitchbotModel.CIRCULATOR_FAN,
79
79
  "WoHub2": SwitchbotModel.HUB2,
80
80
  "WoRollerShade": SwitchbotModel.ROLLER_SHADE,
81
- "WoAirPurifierJP": SwitchbotModel.AIR_PURIFIER,
82
- "WoAirPurifierUS": SwitchbotModel.AIR_PURIFIER,
83
- "WoAirPurifierJPPro": SwitchbotModel.AIR_PURIFIER_TABLE,
84
- "WoAirPurifierUSPro": SwitchbotModel.AIR_PURIFIER_TABLE,
81
+ "WoAirPurifierJP": SwitchbotModel.AIR_PURIFIER_JP,
82
+ "WoAirPurifierUS": SwitchbotModel.AIR_PURIFIER_US,
83
+ "WoAirPurifierJPPro": SwitchbotModel.AIR_PURIFIER_TABLE_JP,
84
+ "WoAirPurifierUSPro": SwitchbotModel.AIR_PURIFIER_TABLE_US,
85
85
  "WoSweeperMini": SwitchbotModel.K10_VACUUM,
86
86
  "WoSweeperMiniPro": SwitchbotModel.K10_PRO_VACUUM,
87
87
  "91AgWZ1n": SwitchbotModel.K10_PRO_COMBO_VACUUM,
@@ -106,6 +106,9 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
106
106
  "W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
107
107
  "W1111000": SwitchbotModel.CLIMATE_PANEL,
108
108
  "W1130000": SwitchbotModel.ART_FRAME,
109
+ "W1141001": SwitchbotModel.LOCK_VISION_PRO,
110
+ "W1141000": SwitchbotModel.LOCK_VISION,
111
+ "W1114000": SwitchbotModel.LOCK_PRO_WIFI,
109
112
  }
110
113
 
111
114
  REQ_HEADER = "570f"
@@ -213,6 +216,8 @@ class SwitchbotBaseDevice:
213
216
  _open_command: str | None = None
214
217
  _close_command: str | None = None
215
218
  _press_command: str | None = None
219
+ _open_child_lock_command: str | None = None
220
+ _close_child_lock_command: str | None = None
216
221
 
217
222
  def __init__(
218
223
  self,
@@ -932,6 +937,20 @@ class SwitchbotBaseDevice:
932
937
  result = await self._send_command(self._press_command)
933
938
  return self._check_command_result(result, 0, {1})
934
939
 
940
+ @update_after_operation
941
+ async def open_child_lock(self) -> bool:
942
+ """Open the child lock."""
943
+ self._check_function_support(self._open_child_lock_command)
944
+ result = await self._send_command(self._open_child_lock_command)
945
+ return self._check_command_result(result, 0, {1})
946
+
947
+ @update_after_operation
948
+ async def close_child_lock(self) -> bool:
949
+ """Close the child lock."""
950
+ self._check_function_support(self._close_child_lock_command)
951
+ result = await self._send_command(self._close_child_lock_command)
952
+ return self._check_command_result(result, 0, {1})
953
+
935
954
 
936
955
  class SwitchbotDevice(SwitchbotBaseDevice):
937
956
  """