PySwitchbot 1.1.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 (114) hide show
  1. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/PKG-INFO +1 -1
  2. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/setup.py +1 -1
  4. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/__init__.py +2 -0
  5. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parser.py +16 -16
  6. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/__init__.py +6 -3
  7. pyswitchbot-2.0.0/switchbot/devices/air_purifier.py +311 -0
  8. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/base_light.py +26 -8
  9. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/device.py +20 -4
  10. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_adv_parser.py +17 -15
  11. pyswitchbot-2.0.0/tests/test_air_purifier.py +484 -0
  12. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_bulb.py +8 -18
  13. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_ceiling_light.py +8 -18
  14. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_lock.py +1 -1
  15. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_strip_light.py +8 -16
  16. pyswitchbot-1.1.0/switchbot/devices/air_purifier.py +0 -131
  17. pyswitchbot-1.1.0/tests/test_air_purifier.py +0 -231
  18. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/LICENSE +0 -0
  19. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/MANIFEST.in +0 -0
  20. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/SOURCES.txt +0 -0
  21. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  22. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/requires.txt +0 -0
  23. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  24. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/README.md +0 -0
  25. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/pyproject.toml +0 -0
  26. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/setup.cfg +0 -0
  27. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/__init__.py +0 -0
  28. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  29. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/art_frame.py +0 -0
  30. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  31. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/bot.py +0 -0
  32. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/bulb.py +0 -0
  33. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  34. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/climate_panel.py +0 -0
  35. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/contact.py +0 -0
  36. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/curtain.py +0 -0
  37. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/fan.py +0 -0
  38. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/hub2.py +0 -0
  39. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/hub3.py +0 -0
  40. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  41. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/humidifier.py +0 -0
  42. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/keypad.py +0 -0
  43. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/keypad_vision.py +0 -0
  44. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/leak.py +0 -0
  45. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/light_strip.py +0 -0
  46. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/lock.py +0 -0
  47. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/meter.py +0 -0
  48. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/motion.py +0 -0
  49. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/plug.py +0 -0
  50. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/presence_sensor.py +0 -0
  51. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  52. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/remote.py +0 -0
  53. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  54. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
  55. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/adv_parsers/vacuum.py +0 -0
  56. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/api_config.py +0 -0
  57. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/air_purifier.py +0 -0
  58. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/climate.py +0 -0
  59. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/evaporative_humidifier.py +0 -0
  60. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/fan.py +0 -0
  61. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/hub2.py +0 -0
  62. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/hub3.py +0 -0
  63. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/light.py +0 -0
  64. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/lock.py +0 -0
  65. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/const/presence_sensor.py +0 -0
  66. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/__init__.py +0 -0
  67. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/art_frame.py +0 -0
  68. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/base_cover.py +0 -0
  69. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/blind_tilt.py +0 -0
  70. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/bot.py +0 -0
  71. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/bulb.py +0 -0
  72. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/ceiling_light.py +0 -0
  73. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/contact.py +0 -0
  74. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/curtain.py +0 -0
  75. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/evaporative_humidifier.py +0 -0
  76. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/fan.py +0 -0
  77. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/humidifier.py +0 -0
  78. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/keypad.py +0 -0
  79. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/keypad_vision.py +0 -0
  80. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/light_strip.py +0 -0
  81. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/lock.py +0 -0
  82. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/meter.py +0 -0
  83. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/meter_pro.py +0 -0
  84. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/motion.py +0 -0
  85. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/plug.py +0 -0
  86. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/relay_switch.py +0 -0
  87. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/roller_shade.py +0 -0
  88. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/smart_thermostat_radiator.py +0 -0
  89. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/devices/vacuum.py +0 -0
  90. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/discovery.py +0 -0
  91. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/enum.py +0 -0
  92. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/helpers.py +0 -0
  93. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/models.py +0 -0
  94. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/switchbot/utils.py +0 -0
  95. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_art_frame.py +0 -0
  96. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_base_cover.py +0 -0
  97. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_blind_tilt.py +0 -0
  98. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_colormode_imports.py +0 -0
  99. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_curtain.py +0 -0
  100. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_device.py +0 -0
  101. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_discovery_callback.py +0 -0
  102. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_encrypted_device.py +0 -0
  103. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_evaporative_humidifier.py +0 -0
  104. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_fan.py +0 -0
  105. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_helpers.py +0 -0
  106. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_hub2.py +0 -0
  107. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_hub3.py +0 -0
  108. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_keypad_vision.py +0 -0
  109. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_meter_pro.py +0 -0
  110. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_relay_switch.py +0 -0
  111. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_roller_shade.py +0 -0
  112. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_smart_thermostat_radiator.py +0 -0
  113. {pyswitchbot-1.1.0 → pyswitchbot-2.0.0}/tests/test_utils.py +0 -0
  114. {pyswitchbot-1.1.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.1.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.1.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
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="1.1.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
  },
@@ -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"
@@ -115,6 +117,7 @@ __all__ = [
115
117
  "DEFAULT_RETRY_TIMEOUT",
116
118
  "DEFAULT_SCAN_TIMEOUT",
117
119
  "AirPurifierMode",
120
+ "AirQualityLevel",
118
121
  "BulbColorMode",
119
122
  "CeilingLightColorMode",
120
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,
@@ -216,6 +216,8 @@ class SwitchbotBaseDevice:
216
216
  _open_command: str | None = None
217
217
  _close_command: str | None = None
218
218
  _press_command: str | None = None
219
+ _open_child_lock_command: str | None = None
220
+ _close_child_lock_command: str | None = None
219
221
 
220
222
  def __init__(
221
223
  self,
@@ -935,6 +937,20 @@ class SwitchbotBaseDevice:
935
937
  result = await self._send_command(self._press_command)
936
938
  return self._check_command_result(result, 0, {1})
937
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
+
938
954
 
939
955
  class SwitchbotDevice(SwitchbotBaseDevice):
940
956
  """
@@ -2364,8 +2364,8 @@ def test_s10_with_empty_data() -> None:
2364
2364
  "sequence_number": 161,
2365
2365
  },
2366
2366
  "7",
2367
- "Air Purifier Table",
2368
- SwitchbotModel.AIR_PURIFIER_TABLE,
2367
+ "Air Purifier Table US",
2368
+ SwitchbotModel.AIR_PURIFIER_TABLE_US,
2369
2369
  ),
2370
2370
  AdvTestCase(
2371
2371
  b'\xcc\x8d\xa2\xa7\x92>\t"\x80\x000\x00\x0f\x00\x00',
@@ -2382,8 +2382,8 @@ def test_s10_with_empty_data() -> None:
2382
2382
  "sequence_number": 9,
2383
2383
  },
2384
2384
  "*",
2385
- "Air Purifier",
2386
- SwitchbotModel.AIR_PURIFIER,
2385
+ "Air Purifier US",
2386
+ SwitchbotModel.AIR_PURIFIER_US,
2387
2387
  ),
2388
2388
  AdvTestCase(
2389
2389
  b"\xcc\x8d\xa2\xa7\xe4\xa6\x0b\x83\x88d\x00\xea`\x00\x00",
@@ -2400,8 +2400,8 @@ def test_s10_with_empty_data() -> None:
2400
2400
  "sequence_number": 11,
2401
2401
  },
2402
2402
  "+",
2403
- "Air Purifier",
2404
- SwitchbotModel.AIR_PURIFIER,
2403
+ "Air Purifier JP",
2404
+ SwitchbotModel.AIR_PURIFIER_JP,
2405
2405
  ),
2406
2406
  AdvTestCase(
2407
2407
  b"\xcc\x8d\xa2\xa7\xc1\xae\x9b\x81\x8c\xb2\x00\x01\x94\x00\x00",
@@ -2418,8 +2418,8 @@ def test_s10_with_empty_data() -> None:
2418
2418
  "sequence_number": 155,
2419
2419
  },
2420
2420
  "8",
2421
- "Air Purifier Table",
2422
- SwitchbotModel.AIR_PURIFIER_TABLE,
2421
+ "Air Purifier Table JP",
2422
+ SwitchbotModel.AIR_PURIFIER_TABLE_JP,
2423
2423
  ),
2424
2424
  AdvTestCase(
2425
2425
  b"\xcc\x8d\xa2\xa7\xc1\xae\x9e\xa1\x8c\x800\x01\x95\x00\x00",
@@ -2436,8 +2436,8 @@ def test_s10_with_empty_data() -> None:
2436
2436
  "sequence_number": 158,
2437
2437
  },
2438
2438
  "8",
2439
- "Air Purifier Table",
2440
- SwitchbotModel.AIR_PURIFIER_TABLE,
2439
+ "Air Purifier Table JP",
2440
+ SwitchbotModel.AIR_PURIFIER_TABLE_JP,
2441
2441
  ),
2442
2442
  AdvTestCase(
2443
2443
  b"\xcc\x8d\xa2\xa7\xc1\xae\x9e\x05\x8c\x800\x01\x95\x00\x00",
@@ -2454,8 +2454,8 @@ def test_s10_with_empty_data() -> None:
2454
2454
  "sequence_number": 158,
2455
2455
  },
2456
2456
  "8",
2457
- "Air Purifier Table",
2458
- SwitchbotModel.AIR_PURIFIER_TABLE,
2457
+ "Air Purifier Table JP",
2458
+ SwitchbotModel.AIR_PURIFIER_TABLE_JP,
2459
2459
  ),
2460
2460
  ],
2461
2461
  )
@@ -2491,7 +2491,9 @@ def test_air_purifier_passive() -> None:
2491
2491
  },
2492
2492
  rssi=-97,
2493
2493
  )
2494
- result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.AIR_PURIFIER)
2494
+ result = parse_advertisement_data(
2495
+ ble_device, adv_data, SwitchbotModel.AIR_PURIFIER_US
2496
+ )
2495
2497
  assert result == SwitchBotAdvertisement(
2496
2498
  address="aa:bb:cc:dd:ee:ff",
2497
2499
  data={
@@ -2509,8 +2511,8 @@ def test_air_purifier_passive() -> None:
2509
2511
  },
2510
2512
  "isEncrypted": False,
2511
2513
  "model": "*",
2512
- "modelFriendlyName": "Air Purifier",
2513
- "modelName": SwitchbotModel.AIR_PURIFIER,
2514
+ "modelFriendlyName": "Air Purifier US",
2515
+ "modelName": SwitchbotModel.AIR_PURIFIER_US,
2514
2516
  },
2515
2517
  device=ble_device,
2516
2518
  rssi=-97,