PySwitchbot 0.61.0__tar.gz → 0.62.1__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.
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/PKG-INFO +2 -2
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/PySwitchbot.egg-info/PKG-INFO +2 -2
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/PySwitchbot.egg-info/SOURCES.txt +4 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/README.md +1 -1
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/setup.py +1 -1
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/__init__.py +4 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parser.py +25 -0
- pyswitchbot-0.62.1/switchbot/adv_parsers/air_purifier.py +52 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/const/__init__.py +4 -0
- pyswitchbot-0.62.1/switchbot/const/air_purifier.py +23 -0
- pyswitchbot-0.62.1/switchbot/devices/air_purifier.py +142 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_adv_parser.py +199 -0
- pyswitchbot-0.62.1/tests/test_air_purifier.py +231 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/LICENSE +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/MANIFEST.in +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/pyproject.toml +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/setup.cfg +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/device.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/fan.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/relay_switch.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/enum.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/switchbot/models.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_fan.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_relay_switch.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_roller_shade.py +0 -0
- {pyswitchbot-0.61.0 → pyswitchbot-0.62.1}/tests/test_vacuum.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PySwitchbot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.62.1
|
|
4
4
|
Summary: A library to communicate with Switchbot
|
|
5
5
|
Home-page: https://github.com/sblibs/pySwitchbot/
|
|
6
6
|
Author: Daniel Hjelseth Hoyer
|
|
@@ -31,7 +31,7 @@ Dynamic: requires-dist
|
|
|
31
31
|
Dynamic: requires-python
|
|
32
32
|
Dynamic: summary
|
|
33
33
|
|
|
34
|
-
# pySwitchbot [](https://codecov.io/gh/sblibs/pySwitchbot)
|
|
35
35
|
|
|
36
36
|
Library to control Switchbot IoT devices https://www.switch-bot.com/bot
|
|
37
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PySwitchbot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.62.1
|
|
4
4
|
Summary: A library to communicate with Switchbot
|
|
5
5
|
Home-page: https://github.com/sblibs/pySwitchbot/
|
|
6
6
|
Author: Daniel Hjelseth Hoyer
|
|
@@ -31,7 +31,7 @@ Dynamic: requires-dist
|
|
|
31
31
|
Dynamic: requires-python
|
|
32
32
|
Dynamic: summary
|
|
33
33
|
|
|
34
|
-
# pySwitchbot [](https://codecov.io/gh/sblibs/pySwitchbot)
|
|
35
35
|
|
|
36
36
|
Library to control Switchbot IoT devices https://www.switch-bot.com/bot
|
|
37
37
|
|
|
@@ -16,6 +16,7 @@ switchbot/enum.py
|
|
|
16
16
|
switchbot/helpers.py
|
|
17
17
|
switchbot/models.py
|
|
18
18
|
switchbot/adv_parsers/__init__.py
|
|
19
|
+
switchbot/adv_parsers/air_purifier.py
|
|
19
20
|
switchbot/adv_parsers/blind_tilt.py
|
|
20
21
|
switchbot/adv_parsers/bot.py
|
|
21
22
|
switchbot/adv_parsers/bulb.py
|
|
@@ -38,11 +39,13 @@ switchbot/adv_parsers/remote.py
|
|
|
38
39
|
switchbot/adv_parsers/roller_shade.py
|
|
39
40
|
switchbot/adv_parsers/vacuum.py
|
|
40
41
|
switchbot/const/__init__.py
|
|
42
|
+
switchbot/const/air_purifier.py
|
|
41
43
|
switchbot/const/evaporative_humidifier.py
|
|
42
44
|
switchbot/const/fan.py
|
|
43
45
|
switchbot/const/hub2.py
|
|
44
46
|
switchbot/const/lock.py
|
|
45
47
|
switchbot/devices/__init__.py
|
|
48
|
+
switchbot/devices/air_purifier.py
|
|
46
49
|
switchbot/devices/base_cover.py
|
|
47
50
|
switchbot/devices/base_light.py
|
|
48
51
|
switchbot/devices/blind_tilt.py
|
|
@@ -65,6 +68,7 @@ switchbot/devices/relay_switch.py
|
|
|
65
68
|
switchbot/devices/roller_shade.py
|
|
66
69
|
switchbot/devices/vacuum.py
|
|
67
70
|
tests/test_adv_parser.py
|
|
71
|
+
tests/test_air_purifier.py
|
|
68
72
|
tests/test_base_cover.py
|
|
69
73
|
tests/test_blind_tilt.py
|
|
70
74
|
tests/test_curtain.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pySwitchbot [](https://codecov.io/gh/sblibs/pySwitchbot)
|
|
2
2
|
|
|
3
3
|
Library to control Switchbot IoT devices https://www.switch-bot.com/bot
|
|
4
4
|
|
|
@@ -10,6 +10,7 @@ from bleak_retry_connector import (
|
|
|
10
10
|
|
|
11
11
|
from .adv_parser import SwitchbotSupportedType, parse_advertisement_data
|
|
12
12
|
from .const import (
|
|
13
|
+
AirPurifierMode,
|
|
13
14
|
FanMode,
|
|
14
15
|
LockStatus,
|
|
15
16
|
SwitchbotAccountConnectionError,
|
|
@@ -17,6 +18,7 @@ from .const import (
|
|
|
17
18
|
SwitchbotAuthenticationError,
|
|
18
19
|
SwitchbotModel,
|
|
19
20
|
)
|
|
21
|
+
from .devices.air_purifier import SwitchbotAirPurifier
|
|
20
22
|
from .devices.base_light import SwitchbotBaseLight
|
|
21
23
|
from .devices.blind_tilt import SwitchbotBlindTilt
|
|
22
24
|
from .devices.bot import Switchbot
|
|
@@ -37,6 +39,7 @@ from .discovery import GetSwitchbotDevices
|
|
|
37
39
|
from .models import SwitchBotAdvertisement
|
|
38
40
|
|
|
39
41
|
__all__ = [
|
|
42
|
+
"AirPurifierMode",
|
|
40
43
|
"ColorMode",
|
|
41
44
|
"FanMode",
|
|
42
45
|
"GetSwitchbotDevices",
|
|
@@ -45,6 +48,7 @@ __all__ = [
|
|
|
45
48
|
"Switchbot",
|
|
46
49
|
"Switchbot",
|
|
47
50
|
"SwitchbotAccountConnectionError",
|
|
51
|
+
"SwitchbotAirPurifier",
|
|
48
52
|
"SwitchbotApiError",
|
|
49
53
|
"SwitchbotAuthenticationError",
|
|
50
54
|
"SwitchbotBaseLight",
|
|
@@ -10,6 +10,7 @@ from typing import Any, TypedDict
|
|
|
10
10
|
from bleak.backends.device import BLEDevice
|
|
11
11
|
from bleak.backends.scanner import AdvertisementData
|
|
12
12
|
|
|
13
|
+
from .adv_parsers.air_purifier import process_air_purifier
|
|
13
14
|
from .adv_parsers.blind_tilt import process_woblindtilt
|
|
14
15
|
from .adv_parsers.bot import process_wohand
|
|
15
16
|
from .adv_parsers.bulb import process_color_bulb
|
|
@@ -268,6 +269,30 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
|
|
|
268
269
|
"func": process_vacuum_k,
|
|
269
270
|
"manufacturer_id": 2409,
|
|
270
271
|
},
|
|
272
|
+
"*": {
|
|
273
|
+
"modelName": SwitchbotModel.AIR_PURIFIER,
|
|
274
|
+
"modelFriendlyName": "Air Purifier",
|
|
275
|
+
"func": process_air_purifier,
|
|
276
|
+
"manufacturer_id": 2409,
|
|
277
|
+
},
|
|
278
|
+
"+": {
|
|
279
|
+
"modelName": SwitchbotModel.AIR_PURIFIER,
|
|
280
|
+
"modelFriendlyName": "Air Purifier",
|
|
281
|
+
"func": process_air_purifier,
|
|
282
|
+
"manufacturer_id": 2409,
|
|
283
|
+
},
|
|
284
|
+
"7": {
|
|
285
|
+
"modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
|
|
286
|
+
"modelFriendlyName": "Air Purifier Table",
|
|
287
|
+
"func": process_air_purifier,
|
|
288
|
+
"manufacturer_id": 2409,
|
|
289
|
+
},
|
|
290
|
+
"8": {
|
|
291
|
+
"modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
|
|
292
|
+
"modelFriendlyName": "Air Purifier Table",
|
|
293
|
+
"func": process_air_purifier,
|
|
294
|
+
"manufacturer_id": 2409,
|
|
295
|
+
},
|
|
271
296
|
}
|
|
272
297
|
|
|
273
298
|
_SWITCHBOT_MODEL_TO_CHAR = {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Air Purifier adv parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
from ..const.air_purifier import AirPurifierMode, AirQualityLevel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def process_air_purifier(
|
|
11
|
+
data: bytes | None, mfr_data: bytes | None
|
|
12
|
+
) -> dict[str, bool | int]:
|
|
13
|
+
"""Process air purifier services data."""
|
|
14
|
+
if mfr_data is None:
|
|
15
|
+
return {}
|
|
16
|
+
device_data = mfr_data[6:]
|
|
17
|
+
|
|
18
|
+
_seq_num = device_data[0]
|
|
19
|
+
_isOn = bool(device_data[1] & 0b10000000)
|
|
20
|
+
_mode = device_data[1] & 0b00000111
|
|
21
|
+
_is_aqi_valid = bool(device_data[2] & 0b00000100)
|
|
22
|
+
_child_lock = bool(device_data[2] & 0b00000010)
|
|
23
|
+
_speed = device_data[3] & 0b01111111
|
|
24
|
+
_aqi_level = (device_data[4] & 0b00000110) >> 1
|
|
25
|
+
_aqi_level = AirQualityLevel(_aqi_level).name.lower()
|
|
26
|
+
_work_time = struct.unpack(">H", device_data[5:7])[0]
|
|
27
|
+
_err_code = device_data[7]
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
"isOn": _isOn,
|
|
31
|
+
"mode": get_air_purifier_mode(_mode, _speed),
|
|
32
|
+
"isAqiValid": _is_aqi_valid,
|
|
33
|
+
"child_lock": _child_lock,
|
|
34
|
+
"speed": _speed,
|
|
35
|
+
"aqi_level": _aqi_level,
|
|
36
|
+
"filter element working time": _work_time,
|
|
37
|
+
"err_code": _err_code,
|
|
38
|
+
"sequence_number": _seq_num,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_air_purifier_mode(mode: int, speed: int) -> str | None:
|
|
43
|
+
if mode == 1:
|
|
44
|
+
if 0 <= speed <= 33:
|
|
45
|
+
return "level_1"
|
|
46
|
+
if 34 <= speed <= 66:
|
|
47
|
+
return "level_2"
|
|
48
|
+
return "level_3"
|
|
49
|
+
if 1 < mode <= 4:
|
|
50
|
+
mode += 2
|
|
51
|
+
return AirPurifierMode(mode).name.lower()
|
|
52
|
+
return None
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from ..enum import StrEnum
|
|
6
|
+
from .air_purifier import AirPurifierMode
|
|
6
7
|
from .fan import FanMode
|
|
7
8
|
|
|
8
9
|
# Preserve old LockStatus export for backwards compatibility
|
|
@@ -72,12 +73,15 @@ class SwitchbotModel(StrEnum):
|
|
|
72
73
|
K10_VACUUM = "K10+ Vacuum"
|
|
73
74
|
K10_PRO_VACUUM = "K10+ Pro Vacuum"
|
|
74
75
|
K10_PRO_COMBO_VACUUM = "K10+ Pro Combo Vacuum"
|
|
76
|
+
AIR_PURIFIER = "Air Purifier"
|
|
77
|
+
AIR_PURIFIER_TABLE = "Air Purifier Table"
|
|
75
78
|
|
|
76
79
|
|
|
77
80
|
__all__ = [
|
|
78
81
|
"DEFAULT_RETRY_COUNT",
|
|
79
82
|
"DEFAULT_RETRY_TIMEOUT",
|
|
80
83
|
"DEFAULT_SCAN_TIMEOUT",
|
|
84
|
+
"AirPurifierMode",
|
|
81
85
|
"FanMode",
|
|
82
86
|
"LockStatus",
|
|
83
87
|
"SwitchbotAccountConnectionError",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AirPurifierMode(Enum):
|
|
7
|
+
LEVEL_1 = 1
|
|
8
|
+
LEVEL_2 = 2
|
|
9
|
+
LEVEL_3 = 3
|
|
10
|
+
AUTO = 4
|
|
11
|
+
PET = 5
|
|
12
|
+
SLEEP = 6
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def get_modes(cls) -> list[str]:
|
|
16
|
+
return [mode.name.lower() for mode in cls]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AirQualityLevel(Enum):
|
|
20
|
+
EXCELLENT = 0
|
|
21
|
+
GOOD = 1
|
|
22
|
+
MODERATE = 2
|
|
23
|
+
UNHEALTHY = 3
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Library to handle connection with Switchbot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import struct
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from bleak.backends.device import BLEDevice
|
|
10
|
+
|
|
11
|
+
from ..adv_parsers.air_purifier import get_air_purifier_mode
|
|
12
|
+
from ..const import SwitchbotModel
|
|
13
|
+
from ..const.air_purifier import AirPurifierMode, AirQualityLevel
|
|
14
|
+
from .device import (
|
|
15
|
+
SwitchbotEncryptedDevice,
|
|
16
|
+
SwitchbotSequenceDevice,
|
|
17
|
+
update_after_operation,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
COMMAND_HEAD = "570f4c"
|
|
24
|
+
COMMAND_TURN_OFF = f"{COMMAND_HEAD}010000"
|
|
25
|
+
COMMAND_TURN_ON = f"{COMMAND_HEAD}010100"
|
|
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.PET.name.lower(): f"{COMMAND_HEAD}01010300",
|
|
32
|
+
AirPurifierMode.SLEEP.name.lower(): f"{COMMAND_HEAD}01010400",
|
|
33
|
+
}
|
|
34
|
+
DEVICE_GET_BASIC_SETTINGS_KEY = "570f4d81"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SwitchbotAirPurifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
38
|
+
"""Representation of a Switchbot Air Purifier."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
device: BLEDevice,
|
|
43
|
+
key_id: str,
|
|
44
|
+
encryption_key: str,
|
|
45
|
+
interface: int = 0,
|
|
46
|
+
model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER,
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> None:
|
|
49
|
+
super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
async def verify_encryption_key(
|
|
53
|
+
cls,
|
|
54
|
+
device: BLEDevice,
|
|
55
|
+
key_id: str,
|
|
56
|
+
encryption_key: str,
|
|
57
|
+
model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER,
|
|
58
|
+
**kwargs: Any,
|
|
59
|
+
) -> bool:
|
|
60
|
+
return await super().verify_encryption_key(
|
|
61
|
+
device, key_id, encryption_key, model, **kwargs
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
65
|
+
"""Get device basic settings."""
|
|
66
|
+
if not (_data := await self._get_basic_info()):
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
_LOGGER.debug("data: %s", _data)
|
|
70
|
+
isOn = bool(_data[2] & 0b10000000)
|
|
71
|
+
version_info = (_data[2] & 0b00110000) >> 4
|
|
72
|
+
_mode = _data[2] & 0b00000111
|
|
73
|
+
isAqiValid = bool(_data[3] & 0b00000100)
|
|
74
|
+
child_lock = bool(_data[3] & 0b00000010)
|
|
75
|
+
_aqi_level = (_data[4] & 0b00000110) >> 1
|
|
76
|
+
aqi_level = AirQualityLevel(_aqi_level).name.lower()
|
|
77
|
+
speed = _data[6] & 0b01111111
|
|
78
|
+
pm25 = struct.unpack("<H", _data[12:14])[0] & 0xFFF
|
|
79
|
+
firmware = _data[15] / 10.0
|
|
80
|
+
mode = get_air_purifier_mode(_mode, speed)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"isOn": isOn,
|
|
84
|
+
"version_info": version_info,
|
|
85
|
+
"mode": mode,
|
|
86
|
+
"isAqiValid": isAqiValid,
|
|
87
|
+
"child_lock": child_lock,
|
|
88
|
+
"aqi_level": aqi_level,
|
|
89
|
+
"speed": speed,
|
|
90
|
+
"pm25": pm25,
|
|
91
|
+
"firmware": firmware,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async def _get_basic_info(self) -> bytes | None:
|
|
95
|
+
"""Return basic info of device."""
|
|
96
|
+
_data = await self._send_command(
|
|
97
|
+
key=DEVICE_GET_BASIC_SETTINGS_KEY, retry=self._retry_count
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if _data in (b"\x07", b"\x00"):
|
|
101
|
+
_LOGGER.error("Unsuccessful, please try again")
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
return _data
|
|
105
|
+
|
|
106
|
+
@update_after_operation
|
|
107
|
+
async def set_preset_mode(self, preset_mode: str) -> bool:
|
|
108
|
+
"""Send command to set air purifier preset_mode."""
|
|
109
|
+
result = await self._send_command(COMMAND_SET_MODE[preset_mode])
|
|
110
|
+
return self._check_command_result(result, 0, {1})
|
|
111
|
+
|
|
112
|
+
@update_after_operation
|
|
113
|
+
async def turn_on(self) -> bool:
|
|
114
|
+
"""Turn on the air purifier."""
|
|
115
|
+
result = await self._send_command(COMMAND_TURN_ON)
|
|
116
|
+
return self._check_command_result(result, 0, {1})
|
|
117
|
+
|
|
118
|
+
@update_after_operation
|
|
119
|
+
async def turn_off(self) -> bool:
|
|
120
|
+
"""Turn off the air purifier."""
|
|
121
|
+
result = await self._send_command(COMMAND_TURN_OFF)
|
|
122
|
+
return self._check_command_result(result, 0, {1})
|
|
123
|
+
|
|
124
|
+
def get_current_percentage(self) -> Any:
|
|
125
|
+
"""Return cached percentage."""
|
|
126
|
+
return self._get_adv_value("speed")
|
|
127
|
+
|
|
128
|
+
def is_on(self) -> bool | None:
|
|
129
|
+
"""Return air purifier state from cache."""
|
|
130
|
+
return self._get_adv_value("isOn")
|
|
131
|
+
|
|
132
|
+
def get_current_aqi_level(self) -> Any:
|
|
133
|
+
"""Return cached aqi level."""
|
|
134
|
+
return self._get_adv_value("aqi_level")
|
|
135
|
+
|
|
136
|
+
def get_current_pm25(self) -> Any:
|
|
137
|
+
"""Return cached pm25."""
|
|
138
|
+
return self._get_adv_value("pm25")
|
|
139
|
+
|
|
140
|
+
def get_current_mode(self) -> Any:
|
|
141
|
+
"""Return cached mode."""
|
|
142
|
+
return self._get_adv_value("mode")
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
5
6
|
from bleak.backends.device import BLEDevice
|
|
6
7
|
from bleak.backends.scanner import AdvertisementData
|
|
7
8
|
|
|
@@ -10,6 +11,8 @@ from switchbot.adv_parser import parse_advertisement_data
|
|
|
10
11
|
from switchbot.const.lock import LockStatus
|
|
11
12
|
from switchbot.models import SwitchBotAdvertisement
|
|
12
13
|
|
|
14
|
+
from . import AirPurifierTestCase
|
|
15
|
+
|
|
13
16
|
ADVERTISEMENT_DATA_DEFAULTS = {
|
|
14
17
|
"local_name": "",
|
|
15
18
|
"manufacturer_data": {},
|
|
@@ -2643,3 +2646,199 @@ def test_s10_with_empty_data() -> None:
|
|
|
2643
2646
|
rssi=-97,
|
|
2644
2647
|
active=True,
|
|
2645
2648
|
)
|
|
2649
|
+
|
|
2650
|
+
|
|
2651
|
+
@pytest.mark.parametrize(
|
|
2652
|
+
"test_case",
|
|
2653
|
+
[
|
|
2654
|
+
AirPurifierTestCase(
|
|
2655
|
+
b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00",
|
|
2656
|
+
b"7\x00\x00\x95-\x00",
|
|
2657
|
+
{
|
|
2658
|
+
"isOn": True,
|
|
2659
|
+
"mode": "level_3",
|
|
2660
|
+
"isAqiValid": False,
|
|
2661
|
+
"child_lock": False,
|
|
2662
|
+
"speed": 100,
|
|
2663
|
+
"aqi_level": "excellent",
|
|
2664
|
+
"filter element working time": 405,
|
|
2665
|
+
"err_code": 0,
|
|
2666
|
+
"sequence_number": 161,
|
|
2667
|
+
},
|
|
2668
|
+
"7",
|
|
2669
|
+
"Air Purifier Table",
|
|
2670
|
+
SwitchbotModel.AIR_PURIFIER_TABLE,
|
|
2671
|
+
),
|
|
2672
|
+
AirPurifierTestCase(
|
|
2673
|
+
b'\xcc\x8d\xa2\xa7\x92>\t"\x80\x000\x00\x0f\x00\x00',
|
|
2674
|
+
b"*\x00\x00\x15\x04\x00",
|
|
2675
|
+
{
|
|
2676
|
+
"isOn": False,
|
|
2677
|
+
"mode": "auto",
|
|
2678
|
+
"isAqiValid": False,
|
|
2679
|
+
"child_lock": False,
|
|
2680
|
+
"speed": 0,
|
|
2681
|
+
"aqi_level": "excellent",
|
|
2682
|
+
"filter element working time": 15,
|
|
2683
|
+
"err_code": 0,
|
|
2684
|
+
"sequence_number": 9,
|
|
2685
|
+
},
|
|
2686
|
+
"*",
|
|
2687
|
+
"Air Purifier",
|
|
2688
|
+
SwitchbotModel.AIR_PURIFIER,
|
|
2689
|
+
),
|
|
2690
|
+
AirPurifierTestCase(
|
|
2691
|
+
b"\xcc\x8d\xa2\xa7\xe4\xa6\x0b\x83\x88d\x00\xea`\x00\x00",
|
|
2692
|
+
b"+\x00\x00\x15\x04\x00",
|
|
2693
|
+
{
|
|
2694
|
+
"isOn": True,
|
|
2695
|
+
"mode": "pet",
|
|
2696
|
+
"isAqiValid": False,
|
|
2697
|
+
"child_lock": False,
|
|
2698
|
+
"speed": 100,
|
|
2699
|
+
"aqi_level": "excellent",
|
|
2700
|
+
"filter element working time": 60000,
|
|
2701
|
+
"err_code": 0,
|
|
2702
|
+
"sequence_number": 11,
|
|
2703
|
+
},
|
|
2704
|
+
"+",
|
|
2705
|
+
"Air Purifier",
|
|
2706
|
+
SwitchbotModel.AIR_PURIFIER,
|
|
2707
|
+
),
|
|
2708
|
+
AirPurifierTestCase(
|
|
2709
|
+
b"\xcc\x8d\xa2\xa7\xc1\xae\x9b\x81\x8c\xb2\x00\x01\x94\x00\x00",
|
|
2710
|
+
b"8\x00\x00\x95-\x00",
|
|
2711
|
+
{
|
|
2712
|
+
"isOn": True,
|
|
2713
|
+
"mode": "level_2",
|
|
2714
|
+
"isAqiValid": True,
|
|
2715
|
+
"child_lock": False,
|
|
2716
|
+
"speed": 50,
|
|
2717
|
+
"aqi_level": "excellent",
|
|
2718
|
+
"filter element working time": 404,
|
|
2719
|
+
"err_code": 0,
|
|
2720
|
+
"sequence_number": 155,
|
|
2721
|
+
},
|
|
2722
|
+
"8",
|
|
2723
|
+
"Air Purifier Table",
|
|
2724
|
+
SwitchbotModel.AIR_PURIFIER_TABLE,
|
|
2725
|
+
),
|
|
2726
|
+
AirPurifierTestCase(
|
|
2727
|
+
b"\xcc\x8d\xa2\xa7\xc1\xae\x9e\xa1\x8c\x800\x01\x95\x00\x00",
|
|
2728
|
+
b"8\x00\x00\x95-\x00",
|
|
2729
|
+
{
|
|
2730
|
+
"isOn": True,
|
|
2731
|
+
"mode": "level_1",
|
|
2732
|
+
"isAqiValid": True,
|
|
2733
|
+
"child_lock": False,
|
|
2734
|
+
"speed": 0,
|
|
2735
|
+
"aqi_level": "excellent",
|
|
2736
|
+
"filter element working time": 405,
|
|
2737
|
+
"err_code": 0,
|
|
2738
|
+
"sequence_number": 158,
|
|
2739
|
+
},
|
|
2740
|
+
"8",
|
|
2741
|
+
"Air Purifier Table",
|
|
2742
|
+
SwitchbotModel.AIR_PURIFIER_TABLE,
|
|
2743
|
+
),
|
|
2744
|
+
AirPurifierTestCase(
|
|
2745
|
+
b"\xcc\x8d\xa2\xa7\xc1\xae\x9e\x05\x8c\x800\x01\x95\x00\x00",
|
|
2746
|
+
b"8\x00\x00\x95-\x00",
|
|
2747
|
+
{
|
|
2748
|
+
"isOn": False,
|
|
2749
|
+
"mode": None,
|
|
2750
|
+
"isAqiValid": True,
|
|
2751
|
+
"child_lock": False,
|
|
2752
|
+
"speed": 0,
|
|
2753
|
+
"aqi_level": "excellent",
|
|
2754
|
+
"filter element working time": 405,
|
|
2755
|
+
"err_code": 0,
|
|
2756
|
+
"sequence_number": 158,
|
|
2757
|
+
},
|
|
2758
|
+
"8",
|
|
2759
|
+
"Air Purifier Table",
|
|
2760
|
+
SwitchbotModel.AIR_PURIFIER_TABLE,
|
|
2761
|
+
),
|
|
2762
|
+
],
|
|
2763
|
+
)
|
|
2764
|
+
def test_air_purifier_active(test_case: AirPurifierTestCase) -> None:
|
|
2765
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
2766
|
+
adv_data = generate_advertisement_data(
|
|
2767
|
+
manufacturer_data={2409: test_case.manufacturer_data},
|
|
2768
|
+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": test_case.service_data},
|
|
2769
|
+
rssi=-97,
|
|
2770
|
+
)
|
|
2771
|
+
result = parse_advertisement_data(ble_device, adv_data)
|
|
2772
|
+
assert result == SwitchBotAdvertisement(
|
|
2773
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
2774
|
+
data={
|
|
2775
|
+
"rawAdvData": test_case.service_data,
|
|
2776
|
+
"data": test_case.data,
|
|
2777
|
+
"isEncrypted": False,
|
|
2778
|
+
"model": test_case.model,
|
|
2779
|
+
"modelFriendlyName": test_case.modelFriendlyName,
|
|
2780
|
+
"modelName": test_case.modelName,
|
|
2781
|
+
},
|
|
2782
|
+
device=ble_device,
|
|
2783
|
+
rssi=-97,
|
|
2784
|
+
active=True,
|
|
2785
|
+
)
|
|
2786
|
+
|
|
2787
|
+
|
|
2788
|
+
def test_air_purifier_passive() -> None:
|
|
2789
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
2790
|
+
adv_data = generate_advertisement_data(
|
|
2791
|
+
manufacturer_data={
|
|
2792
|
+
2409: b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00"
|
|
2793
|
+
},
|
|
2794
|
+
rssi=-97,
|
|
2795
|
+
)
|
|
2796
|
+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.AIR_PURIFIER)
|
|
2797
|
+
assert result == SwitchBotAdvertisement(
|
|
2798
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
2799
|
+
data={
|
|
2800
|
+
"rawAdvData": None,
|
|
2801
|
+
"data": {
|
|
2802
|
+
"isOn": True,
|
|
2803
|
+
"mode": "level_3",
|
|
2804
|
+
"isAqiValid": False,
|
|
2805
|
+
"child_lock": False,
|
|
2806
|
+
"speed": 100,
|
|
2807
|
+
"aqi_level": "excellent",
|
|
2808
|
+
"filter element working time": 405,
|
|
2809
|
+
"err_code": 0,
|
|
2810
|
+
"sequence_number": 161,
|
|
2811
|
+
},
|
|
2812
|
+
"isEncrypted": False,
|
|
2813
|
+
"model": "+",
|
|
2814
|
+
"modelFriendlyName": "Air Purifier",
|
|
2815
|
+
"modelName": SwitchbotModel.AIR_PURIFIER,
|
|
2816
|
+
},
|
|
2817
|
+
device=ble_device,
|
|
2818
|
+
rssi=-97,
|
|
2819
|
+
active=False,
|
|
2820
|
+
)
|
|
2821
|
+
|
|
2822
|
+
|
|
2823
|
+
def test_air_purifier_with_empty_data() -> None:
|
|
2824
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
2825
|
+
adv_data = generate_advertisement_data(
|
|
2826
|
+
manufacturer_data={2409: None},
|
|
2827
|
+
service_data={
|
|
2828
|
+
"0000fd3d-0000-1000-8000-00805f9b34fb": b"+\x00\x00\x15\x04\x00",
|
|
2829
|
+
},
|
|
2830
|
+
rssi=-97,
|
|
2831
|
+
)
|
|
2832
|
+
result = parse_advertisement_data(ble_device, adv_data)
|
|
2833
|
+
assert result == SwitchBotAdvertisement(
|
|
2834
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
2835
|
+
data={
|
|
2836
|
+
"rawAdvData": b"+\x00\x00\x15\x04\x00",
|
|
2837
|
+
"data": {},
|
|
2838
|
+
"isEncrypted": False,
|
|
2839
|
+
"model": "+",
|
|
2840
|
+
},
|
|
2841
|
+
device=ble_device,
|
|
2842
|
+
rssi=-97,
|
|
2843
|
+
active=True,
|
|
2844
|
+
)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from bleak.backends.device import BLEDevice
|
|
5
|
+
|
|
6
|
+
from switchbot import SwitchBotAdvertisement, SwitchbotEncryptedDevice, SwitchbotModel
|
|
7
|
+
from switchbot.const.air_purifier import AirPurifierMode
|
|
8
|
+
from switchbot.devices import air_purifier
|
|
9
|
+
|
|
10
|
+
from .test_adv_parser import generate_ble_device
|
|
11
|
+
|
|
12
|
+
common_params = [
|
|
13
|
+
(b"7\x00\x00\x95-\x00", "7"),
|
|
14
|
+
(b"*\x00\x00\x15\x04\x00", "*"),
|
|
15
|
+
(b"+\x00\x00\x15\x04\x00", "+"),
|
|
16
|
+
(b"8\x00\x00\x95-\x00", "8"),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_device_for_command_testing(
|
|
21
|
+
rawAdvData: bytes, model: str, init_data: dict | None = None
|
|
22
|
+
):
|
|
23
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
24
|
+
device = air_purifier.SwitchbotAirPurifier(
|
|
25
|
+
ble_device, "ff", "ffffffffffffffffffffffffffffffff"
|
|
26
|
+
)
|
|
27
|
+
device.update_from_advertisement(
|
|
28
|
+
make_advertisement_data(ble_device, rawAdvData, model, init_data)
|
|
29
|
+
)
|
|
30
|
+
device._send_command = AsyncMock()
|
|
31
|
+
device._check_command_result = MagicMock()
|
|
32
|
+
device.update = AsyncMock()
|
|
33
|
+
return device
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def make_advertisement_data(
|
|
37
|
+
ble_device: BLEDevice, rawAdvData: bytes, model: str, init_data: dict | None = None
|
|
38
|
+
):
|
|
39
|
+
"""Set advertisement data with defaults."""
|
|
40
|
+
if init_data is None:
|
|
41
|
+
init_data = {}
|
|
42
|
+
|
|
43
|
+
return SwitchBotAdvertisement(
|
|
44
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
45
|
+
data={
|
|
46
|
+
"rawAdvData": rawAdvData,
|
|
47
|
+
"data": {
|
|
48
|
+
"isOn": True,
|
|
49
|
+
"mode": "level_3",
|
|
50
|
+
"isAqiValid": False,
|
|
51
|
+
"child_lock": False,
|
|
52
|
+
"speed": 100,
|
|
53
|
+
"aqi_level": "excellent",
|
|
54
|
+
"filter element working time": 405,
|
|
55
|
+
"err_code": 0,
|
|
56
|
+
"sequence_number": 161,
|
|
57
|
+
}
|
|
58
|
+
| init_data,
|
|
59
|
+
"isEncrypted": False,
|
|
60
|
+
"model": model,
|
|
61
|
+
"modelFriendlyName": "Air Purifier",
|
|
62
|
+
"modelName": SwitchbotModel.AIR_PURIFIER,
|
|
63
|
+
},
|
|
64
|
+
device=ble_device,
|
|
65
|
+
rssi=-80,
|
|
66
|
+
active=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
@pytest.mark.parametrize(
|
|
72
|
+
("rawAdvData", "model"),
|
|
73
|
+
common_params,
|
|
74
|
+
)
|
|
75
|
+
@pytest.mark.parametrize(
|
|
76
|
+
"pm25",
|
|
77
|
+
[150],
|
|
78
|
+
)
|
|
79
|
+
async def test_status_from_proceess_adv(rawAdvData, model, pm25):
|
|
80
|
+
device = create_device_for_command_testing(rawAdvData, model, {"pm25": pm25})
|
|
81
|
+
assert device.get_current_percentage() == 100
|
|
82
|
+
assert device.is_on() is True
|
|
83
|
+
assert device.get_current_aqi_level() == "excellent"
|
|
84
|
+
assert device.get_current_mode() == "level_3"
|
|
85
|
+
assert device.get_current_pm25() == 150
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
@pytest.mark.parametrize(
|
|
90
|
+
("rawAdvData", "model"),
|
|
91
|
+
common_params,
|
|
92
|
+
)
|
|
93
|
+
async def test_get_basic_info_returns_none_when_no_data(rawAdvData, model):
|
|
94
|
+
device = create_device_for_command_testing(rawAdvData, model)
|
|
95
|
+
device._get_basic_info = AsyncMock(return_value=None)
|
|
96
|
+
|
|
97
|
+
assert await device.get_basic_info() is None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
@pytest.mark.parametrize(
|
|
102
|
+
("rawAdvData", "model"),
|
|
103
|
+
common_params,
|
|
104
|
+
)
|
|
105
|
+
@pytest.mark.parametrize(
|
|
106
|
+
"mode", ["level_1", "level_2", "level_3", "auto", "pet", "sleep"]
|
|
107
|
+
)
|
|
108
|
+
async def test_set_preset_mode(rawAdvData, model, mode):
|
|
109
|
+
device = create_device_for_command_testing(rawAdvData, model, {"mode": mode})
|
|
110
|
+
await device.set_preset_mode(mode)
|
|
111
|
+
assert device.get_current_mode() == mode
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio
|
|
115
|
+
@pytest.mark.parametrize(
|
|
116
|
+
("rawAdvData", "model"),
|
|
117
|
+
common_params,
|
|
118
|
+
)
|
|
119
|
+
async def test_turn_on(rawAdvData, model):
|
|
120
|
+
device = create_device_for_command_testing(rawAdvData, model, {"isOn": True})
|
|
121
|
+
await device.turn_on()
|
|
122
|
+
assert device.is_on() is True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
@pytest.mark.parametrize(
|
|
127
|
+
("rawAdvData", "model"),
|
|
128
|
+
common_params,
|
|
129
|
+
)
|
|
130
|
+
async def test_turn_off(rawAdvData, model):
|
|
131
|
+
device = create_device_for_command_testing(rawAdvData, model, {"isOn": False})
|
|
132
|
+
await device.turn_off()
|
|
133
|
+
assert device.is_on() is False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
@pytest.mark.parametrize(
|
|
138
|
+
("rawAdvData", "model"),
|
|
139
|
+
common_params,
|
|
140
|
+
)
|
|
141
|
+
@pytest.mark.parametrize(
|
|
142
|
+
("response", "expected"),
|
|
143
|
+
[
|
|
144
|
+
(b"\x00", None),
|
|
145
|
+
(b"\x07", None),
|
|
146
|
+
(b"\x01\x02\x03", b"\x01\x02\x03"),
|
|
147
|
+
],
|
|
148
|
+
)
|
|
149
|
+
async def test__get_basic_info(rawAdvData, model, response, expected):
|
|
150
|
+
device = create_device_for_command_testing(rawAdvData, model)
|
|
151
|
+
device._send_command = AsyncMock(return_value=response)
|
|
152
|
+
result = await device._get_basic_info()
|
|
153
|
+
assert result == expected
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.asyncio
|
|
157
|
+
@pytest.mark.parametrize(
|
|
158
|
+
("rawAdvData", "model"),
|
|
159
|
+
common_params,
|
|
160
|
+
)
|
|
161
|
+
@pytest.mark.parametrize(
|
|
162
|
+
("basic_info", "result"),
|
|
163
|
+
[
|
|
164
|
+
(
|
|
165
|
+
bytearray(
|
|
166
|
+
b"\x01\xa7\xe9\x8c\x08\x00\xb2\x01\x96\x00\x00\x00\xf0\x00\x00\x17"
|
|
167
|
+
),
|
|
168
|
+
[True, 2, "level_2", True, False, "excellent", 50, 240, 2.3],
|
|
169
|
+
),
|
|
170
|
+
(
|
|
171
|
+
bytearray(
|
|
172
|
+
b"\x01\xa8\xec\x8c\x08\x00\xb2\x01\x96\x00\x00\x00\xf0\x00\x00\x17"
|
|
173
|
+
),
|
|
174
|
+
[True, 2, "sleep", True, False, "excellent", 50, 240, 2.3],
|
|
175
|
+
),
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
async def test_get_basic_info(rawAdvData, model, basic_info, result):
|
|
179
|
+
device = create_device_for_command_testing(rawAdvData, model)
|
|
180
|
+
|
|
181
|
+
async def mock_get_basic_info():
|
|
182
|
+
return basic_info
|
|
183
|
+
|
|
184
|
+
device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
|
|
185
|
+
|
|
186
|
+
info = await device.get_basic_info()
|
|
187
|
+
assert info["isOn"] == result[0]
|
|
188
|
+
assert info["version_info"] == result[1]
|
|
189
|
+
assert info["mode"] == result[2]
|
|
190
|
+
assert info["isAqiValid"] == result[3]
|
|
191
|
+
assert info["child_lock"] == result[4]
|
|
192
|
+
assert info["aqi_level"] == result[5]
|
|
193
|
+
assert info["speed"] == result[6]
|
|
194
|
+
assert info["pm25"] == result[7]
|
|
195
|
+
assert info["firmware"] == result[8]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@pytest.mark.asyncio
|
|
199
|
+
@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
|
|
200
|
+
async def test_verify_encryption_key(mock_parent_verify):
|
|
201
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
202
|
+
key_id = "ff"
|
|
203
|
+
encryption_key = "ffffffffffffffffffffffffffffffff"
|
|
204
|
+
|
|
205
|
+
mock_parent_verify.return_value = True
|
|
206
|
+
|
|
207
|
+
result = await air_purifier.SwitchbotAirPurifier.verify_encryption_key(
|
|
208
|
+
device=ble_device,
|
|
209
|
+
key_id=key_id,
|
|
210
|
+
encryption_key=encryption_key,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
mock_parent_verify.assert_awaited_once_with(
|
|
214
|
+
ble_device,
|
|
215
|
+
key_id,
|
|
216
|
+
encryption_key,
|
|
217
|
+
SwitchbotModel.AIR_PURIFIER,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
assert result is True
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_get_modes():
|
|
224
|
+
assert AirPurifierMode.get_modes() == [
|
|
225
|
+
"level_1",
|
|
226
|
+
"level_2",
|
|
227
|
+
"level_3",
|
|
228
|
+
"auto",
|
|
229
|
+
"pet",
|
|
230
|
+
"sleep",
|
|
231
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|