PySwitchbot 0.56.1__tar.gz → 0.57.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.
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/PKG-INFO +1 -1
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/PySwitchbot.egg-info/SOURCES.txt +3 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/setup.py +1 -1
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/__init__.py +6 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parser.py +7 -1
- pyswitchbot-0.57.0/switchbot/adv_parsers/humidifier.py +93 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/const/__init__.py +1 -0
- pyswitchbot-0.57.0/switchbot/const/evaporative_humidifier.py +34 -0
- pyswitchbot-0.57.0/switchbot/devices/evaporative_humidifier.py +212 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/discovery.py +6 -0
- pyswitchbot-0.57.0/tests/test_evaporative_humidifier.py +202 -0
- pyswitchbot-0.56.1/switchbot/adv_parsers/humidifier.py +0 -33
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/LICENSE +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/README.md +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/pyproject.toml +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/setup.cfg +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/device.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/devices/relay_switch.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/enum.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/tests/test_adv_parser.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.56.1 → pyswitchbot-0.57.0}/tests/test_relay_switch.py +0 -0
|
@@ -34,6 +34,7 @@ switchbot/adv_parsers/plug.py
|
|
|
34
34
|
switchbot/adv_parsers/relay_switch.py
|
|
35
35
|
switchbot/adv_parsers/remote.py
|
|
36
36
|
switchbot/const/__init__.py
|
|
37
|
+
switchbot/const/evaporative_humidifier.py
|
|
37
38
|
switchbot/const/lock.py
|
|
38
39
|
switchbot/devices/__init__.py
|
|
39
40
|
switchbot/devices/base_cover.py
|
|
@@ -45,6 +46,7 @@ switchbot/devices/ceiling_light.py
|
|
|
45
46
|
switchbot/devices/contact.py
|
|
46
47
|
switchbot/devices/curtain.py
|
|
47
48
|
switchbot/devices/device.py
|
|
49
|
+
switchbot/devices/evaporative_humidifier.py
|
|
48
50
|
switchbot/devices/humidifier.py
|
|
49
51
|
switchbot/devices/keypad.py
|
|
50
52
|
switchbot/devices/light_strip.py
|
|
@@ -57,4 +59,5 @@ tests/test_adv_parser.py
|
|
|
57
59
|
tests/test_base_cover.py
|
|
58
60
|
tests/test_blind_tilt.py
|
|
59
61
|
tests/test_curtain.py
|
|
62
|
+
tests/test_evaporative_humidifier.py
|
|
60
63
|
tests/test_relay_switch.py
|
|
@@ -23,6 +23,7 @@ from .devices.bulb import SwitchbotBulb
|
|
|
23
23
|
from .devices.ceiling_light import SwitchbotCeilingLight
|
|
24
24
|
from .devices.curtain import SwitchbotCurtain
|
|
25
25
|
from .devices.device import ColorMode, SwitchbotDevice, SwitchbotEncryptedDevice
|
|
26
|
+
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
|
|
26
27
|
from .devices.humidifier import SwitchbotHumidifier
|
|
27
28
|
from .devices.light_strip import SwitchbotLightStrip
|
|
28
29
|
from .devices.lock import SwitchbotLock
|
|
@@ -37,6 +38,7 @@ __all__ = [
|
|
|
37
38
|
"LockStatus",
|
|
38
39
|
"SwitchBotAdvertisement",
|
|
39
40
|
"Switchbot",
|
|
41
|
+
"Switchbot",
|
|
40
42
|
"SwitchbotAccountConnectionError",
|
|
41
43
|
"SwitchbotApiError",
|
|
42
44
|
"SwitchbotAuthenticationError",
|
|
@@ -47,13 +49,17 @@ __all__ = [
|
|
|
47
49
|
"SwitchbotCurtain",
|
|
48
50
|
"SwitchbotDevice",
|
|
49
51
|
"SwitchbotEncryptedDevice",
|
|
52
|
+
"SwitchbotEvaporativeHumidifier",
|
|
50
53
|
"SwitchbotHumidifier",
|
|
51
54
|
"SwitchbotLightStrip",
|
|
52
55
|
"SwitchbotLock",
|
|
53
56
|
"SwitchbotModel",
|
|
57
|
+
"SwitchbotModel",
|
|
58
|
+
"SwitchbotPlugMini",
|
|
54
59
|
"SwitchbotPlugMini",
|
|
55
60
|
"SwitchbotRelaySwitch",
|
|
56
61
|
"SwitchbotSupportedType",
|
|
62
|
+
"SwitchbotSupportedType",
|
|
57
63
|
"close_stale_connections",
|
|
58
64
|
"close_stale_connections_by_address",
|
|
59
65
|
"get_device",
|
|
@@ -17,7 +17,7 @@ from .adv_parsers.ceiling_light import process_woceiling
|
|
|
17
17
|
from .adv_parsers.contact import process_wocontact
|
|
18
18
|
from .adv_parsers.curtain import process_wocurtain
|
|
19
19
|
from .adv_parsers.hub2 import process_wohub2
|
|
20
|
-
from .adv_parsers.humidifier import process_wohumidifier
|
|
20
|
+
from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
|
|
21
21
|
from .adv_parsers.keypad import process_wokeypad
|
|
22
22
|
from .adv_parsers.leak import process_leak
|
|
23
23
|
from .adv_parsers.light_strip import process_wostrip
|
|
@@ -164,6 +164,12 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
|
|
|
164
164
|
"manufacturer_id": 741,
|
|
165
165
|
"manufacturer_data_length": 6,
|
|
166
166
|
},
|
|
167
|
+
"#": {
|
|
168
|
+
"modelName": SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
|
|
169
|
+
"modelFriendlyName": "Evaporative Humidifier",
|
|
170
|
+
"func": process_evaporative_humidifier,
|
|
171
|
+
"manufacturer_id": 2409,
|
|
172
|
+
},
|
|
167
173
|
"o": {
|
|
168
174
|
"modelName": SwitchbotModel.LOCK,
|
|
169
175
|
"modelFriendlyName": "Lock",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Humidifier adv parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
from ..const.evaporative_humidifier import (
|
|
9
|
+
OVER_HUMIDIFY_PROTECTION_MODES,
|
|
10
|
+
TARGET_HUMIDITY_MODES,
|
|
11
|
+
HumidifierMode,
|
|
12
|
+
HumidifierWaterLevel,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# mfr_data: 943cc68d3d2e
|
|
18
|
+
# data: 650000cd802b6300
|
|
19
|
+
# data: 650000cd802b6300
|
|
20
|
+
# data: 658000c9802b6300
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Low: 658000c5222b6300
|
|
24
|
+
# Med: 658000c5432b6300
|
|
25
|
+
# High: 658000c5642b6300
|
|
26
|
+
def process_wohumidifier(
|
|
27
|
+
data: bytes | None, mfr_data: bytes | None
|
|
28
|
+
) -> dict[str, bool | int]:
|
|
29
|
+
"""Process WoHumi services data."""
|
|
30
|
+
if data is None:
|
|
31
|
+
return {
|
|
32
|
+
"isOn": None,
|
|
33
|
+
"level": None,
|
|
34
|
+
"switchMode": True,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"isOn": bool(data[1]),
|
|
39
|
+
"level": data[4],
|
|
40
|
+
"switchMode": True,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def process_evaporative_humidifier(
|
|
45
|
+
data: bytes | None, mfr_data: bytes | None
|
|
46
|
+
) -> dict[str, bool | int]:
|
|
47
|
+
"""Process WoHumi services data."""
|
|
48
|
+
if mfr_data is None:
|
|
49
|
+
return {
|
|
50
|
+
"isOn": None,
|
|
51
|
+
"mode": None,
|
|
52
|
+
"target_humidity": None,
|
|
53
|
+
"child_lock": None,
|
|
54
|
+
"over_humidify_protection": None,
|
|
55
|
+
"tank_removed": None,
|
|
56
|
+
"tilted_alert": None,
|
|
57
|
+
"filter_missing": None,
|
|
58
|
+
"humidity": None,
|
|
59
|
+
"temperature": None,
|
|
60
|
+
"filter_run_time": None,
|
|
61
|
+
"filter_alert": None,
|
|
62
|
+
"water_level": None,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
is_on = bool(mfr_data[7] & 0b10000000)
|
|
66
|
+
mode = HumidifierMode(mfr_data[7] & 0b00001111)
|
|
67
|
+
filter_run_time = timedelta(hours=int.from_bytes(mfr_data[12:14], byteorder="big"))
|
|
68
|
+
has_humidity = bool(mfr_data[9] & 0b10000000)
|
|
69
|
+
has_temperature = bool(mfr_data[10] & 0b10000000)
|
|
70
|
+
is_tank_removed = bool(mfr_data[8] & 0b00000100)
|
|
71
|
+
return {
|
|
72
|
+
"isOn": is_on,
|
|
73
|
+
"mode": mode if is_on else None,
|
|
74
|
+
"target_humidity": (mfr_data[16] & 0b01111111)
|
|
75
|
+
if is_on and mode in TARGET_HUMIDITY_MODES
|
|
76
|
+
else None,
|
|
77
|
+
"child_lock": bool(mfr_data[8] & 0b00100000),
|
|
78
|
+
"over_humidify_protection": bool(mfr_data[8] & 0b10000000)
|
|
79
|
+
if is_on and mode in OVER_HUMIDIFY_PROTECTION_MODES
|
|
80
|
+
else None,
|
|
81
|
+
"tank_removed": is_tank_removed,
|
|
82
|
+
"tilted_alert": bool(mfr_data[8] & 0b00000010),
|
|
83
|
+
"filter_missing": bool(mfr_data[8] & 0b00000001),
|
|
84
|
+
"humidity": (mfr_data[9] & 0b01111111) if has_humidity else None,
|
|
85
|
+
"temperature": float(mfr_data[10] & 0b01111111) + float(mfr_data[11] >> 4) / 10
|
|
86
|
+
if has_temperature
|
|
87
|
+
else None,
|
|
88
|
+
"filter_run_time": filter_run_time,
|
|
89
|
+
"filter_alert": filter_run_time.days >= 10,
|
|
90
|
+
"water_level": HumidifierWaterLevel(mfr_data[11] & 0b00000011)
|
|
91
|
+
if not is_tank_removed
|
|
92
|
+
else None,
|
|
93
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HumidifierMode(Enum):
|
|
7
|
+
HIGH = 1
|
|
8
|
+
MEDIUM = 2
|
|
9
|
+
LOW = 3
|
|
10
|
+
QUIET = 4
|
|
11
|
+
TARGET_HUMIDITY = 5
|
|
12
|
+
SLEEP = 6
|
|
13
|
+
AUTO = 7
|
|
14
|
+
DRYING_FILTER = 8
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HumidifierWaterLevel(Enum):
|
|
18
|
+
EMPTY = 0
|
|
19
|
+
LOW = 1
|
|
20
|
+
MEDIUM = 2
|
|
21
|
+
HIGH = 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
OVER_HUMIDIFY_PROTECTION_MODES = {
|
|
25
|
+
HumidifierMode.QUIET,
|
|
26
|
+
HumidifierMode.LOW,
|
|
27
|
+
HumidifierMode.MEDIUM,
|
|
28
|
+
HumidifierMode.HIGH,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
TARGET_HUMIDITY_MODES = {
|
|
32
|
+
HumidifierMode.SLEEP,
|
|
33
|
+
HumidifierMode.TARGET_HUMIDITY,
|
|
34
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from bleak.backends.device import BLEDevice
|
|
5
|
+
|
|
6
|
+
from ..const import SwitchbotModel
|
|
7
|
+
from ..const.evaporative_humidifier import (
|
|
8
|
+
TARGET_HUMIDITY_MODES,
|
|
9
|
+
HumidifierMode,
|
|
10
|
+
HumidifierWaterLevel,
|
|
11
|
+
)
|
|
12
|
+
from ..models import SwitchBotAdvertisement
|
|
13
|
+
from .device import SwitchbotEncryptedDevice
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
COMMAND_HEADER = "57"
|
|
18
|
+
COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
|
|
19
|
+
COMMAND_TURN_ON = f"{COMMAND_HEADER}0f430101"
|
|
20
|
+
COMMAND_TURN_OFF = f"{COMMAND_HEADER}0f430100"
|
|
21
|
+
COMMAND_CHILD_LOCK_ON = f"{COMMAND_HEADER}0f430501"
|
|
22
|
+
COMMAND_CHILD_LOCK_OFF = f"{COMMAND_HEADER}0f430500"
|
|
23
|
+
COMMAND_AUTO_DRY_ON = f"{COMMAND_HEADER}0f430a01"
|
|
24
|
+
COMMAND_AUTO_DRY_OFF = f"{COMMAND_HEADER}0f430a02"
|
|
25
|
+
COMMAND_SET_MODE = f"{COMMAND_HEADER}0f4302"
|
|
26
|
+
COMMAND_GET_BASIC_INFO = f"{COMMAND_HEADER}000300"
|
|
27
|
+
|
|
28
|
+
MODES_COMMANDS = {
|
|
29
|
+
HumidifierMode.HIGH: "010100",
|
|
30
|
+
HumidifierMode.MEDIUM: "010200",
|
|
31
|
+
HumidifierMode.LOW: "010300",
|
|
32
|
+
HumidifierMode.QUIET: "010400",
|
|
33
|
+
HumidifierMode.TARGET_HUMIDITY: "0200",
|
|
34
|
+
HumidifierMode.SLEEP: "0300",
|
|
35
|
+
HumidifierMode.AUTO: "040000",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SwitchbotEvaporativeHumidifier(SwitchbotEncryptedDevice):
|
|
40
|
+
"""Representation of a Switchbot Evaporative Humidifier"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
device: BLEDevice,
|
|
45
|
+
key_id: str,
|
|
46
|
+
encryption_key: str,
|
|
47
|
+
interface: int = 0,
|
|
48
|
+
model: SwitchbotModel = SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
|
|
49
|
+
**kwargs: Any,
|
|
50
|
+
) -> None:
|
|
51
|
+
self._force_next_update = False
|
|
52
|
+
super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
async def verify_encryption_key(
|
|
56
|
+
cls,
|
|
57
|
+
device: BLEDevice,
|
|
58
|
+
key_id: str,
|
|
59
|
+
encryption_key: str,
|
|
60
|
+
model: SwitchbotModel = SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> bool:
|
|
63
|
+
return await super().verify_encryption_key(
|
|
64
|
+
device, key_id, encryption_key, model, **kwargs
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
|
|
68
|
+
"""Update device data from advertisement."""
|
|
69
|
+
super().update_from_advertisement(advertisement)
|
|
70
|
+
_LOGGER.debug(
|
|
71
|
+
"%s: update advertisement: %s",
|
|
72
|
+
self.name,
|
|
73
|
+
advertisement,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def _get_basic_info(self) -> bytes | None:
|
|
77
|
+
"""Return basic info of device."""
|
|
78
|
+
_data = await self._send_command(
|
|
79
|
+
key=COMMAND_GET_BASIC_INFO, retry=self._retry_count
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if _data in (b"\x07", b"\x00"):
|
|
83
|
+
_LOGGER.error("Unsuccessful, please try again")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
return _data
|
|
87
|
+
|
|
88
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
89
|
+
"""Get device basic settings."""
|
|
90
|
+
if not (_data := await self._get_basic_info()):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# Not 100% sure about this data, will verify once a firmware update is available
|
|
94
|
+
return {
|
|
95
|
+
"firmware": _data[2] / 10.0,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async def turn_on(self) -> bool:
|
|
99
|
+
"""Turn device on."""
|
|
100
|
+
result = await self._send_command(COMMAND_TURN_ON)
|
|
101
|
+
if ok := self._check_command_result(result, 0, {1}):
|
|
102
|
+
self._override_state({"isOn": True})
|
|
103
|
+
self._fire_callbacks()
|
|
104
|
+
return ok
|
|
105
|
+
|
|
106
|
+
async def turn_off(self) -> bool:
|
|
107
|
+
"""Turn device off."""
|
|
108
|
+
result = await self._send_command(COMMAND_TURN_OFF)
|
|
109
|
+
if ok := self._check_command_result(result, 0, {1}):
|
|
110
|
+
self._override_state({"isOn": False})
|
|
111
|
+
self._fire_callbacks()
|
|
112
|
+
return ok
|
|
113
|
+
|
|
114
|
+
async def set_mode(
|
|
115
|
+
self, mode: HumidifierMode, target_humidity: int | None = None
|
|
116
|
+
) -> bool:
|
|
117
|
+
"""Set device mode."""
|
|
118
|
+
if mode == HumidifierMode.DRYING_FILTER:
|
|
119
|
+
return await self.start_drying_filter()
|
|
120
|
+
elif mode not in MODES_COMMANDS:
|
|
121
|
+
raise ValueError("Invalid mode")
|
|
122
|
+
|
|
123
|
+
command = COMMAND_SET_MODE + MODES_COMMANDS[mode]
|
|
124
|
+
if mode in TARGET_HUMIDITY_MODES:
|
|
125
|
+
if target_humidity is None:
|
|
126
|
+
raise TypeError("target_humidity is required")
|
|
127
|
+
command += f"{target_humidity:02x}"
|
|
128
|
+
result = await self._send_command(command)
|
|
129
|
+
if ok := self._check_command_result(result, 0, {1}):
|
|
130
|
+
self._override_state({"mode": mode})
|
|
131
|
+
if mode == HumidifierMode.TARGET_HUMIDITY and target_humidity is not None:
|
|
132
|
+
self._override_state({"target_humidity": target_humidity})
|
|
133
|
+
self._fire_callbacks()
|
|
134
|
+
return ok
|
|
135
|
+
|
|
136
|
+
async def set_child_lock(self, enabled: bool) -> bool:
|
|
137
|
+
"""Set child lock."""
|
|
138
|
+
result = await self._send_command(
|
|
139
|
+
COMMAND_CHILD_LOCK_ON if enabled else COMMAND_CHILD_LOCK_OFF
|
|
140
|
+
)
|
|
141
|
+
if ok := self._check_command_result(result, 0, {1}):
|
|
142
|
+
self._override_state({"child_lock": enabled})
|
|
143
|
+
self._fire_callbacks()
|
|
144
|
+
return ok
|
|
145
|
+
|
|
146
|
+
async def start_drying_filter(self):
|
|
147
|
+
"""Start drying filter."""
|
|
148
|
+
result = await self._send_command(COMMAND_TURN_ON + "08")
|
|
149
|
+
if ok := self._check_command_result(result, 0, {1}):
|
|
150
|
+
self._override_state({"mode": HumidifierMode.DRYING_FILTER})
|
|
151
|
+
self._fire_callbacks()
|
|
152
|
+
return ok
|
|
153
|
+
|
|
154
|
+
async def stop_drying_filter(self):
|
|
155
|
+
"""Stop drying filter."""
|
|
156
|
+
result = await self._send_command(COMMAND_TURN_OFF)
|
|
157
|
+
if ok := self._check_command_result(result, 0, {0}):
|
|
158
|
+
self._override_state({"isOn": False, "mode": None})
|
|
159
|
+
self._fire_callbacks()
|
|
160
|
+
return ok
|
|
161
|
+
|
|
162
|
+
def is_on(self) -> bool | None:
|
|
163
|
+
"""Return state from cache."""
|
|
164
|
+
return self._get_adv_value("isOn")
|
|
165
|
+
|
|
166
|
+
def get_mode(self) -> HumidifierMode | None:
|
|
167
|
+
"""Return state from cache."""
|
|
168
|
+
return self._get_adv_value("mode")
|
|
169
|
+
|
|
170
|
+
def is_child_lock_enabled(self) -> bool | None:
|
|
171
|
+
"""Return state from cache."""
|
|
172
|
+
return self._get_adv_value("child_lock")
|
|
173
|
+
|
|
174
|
+
def is_over_humidify_protection_enabled(self) -> bool | None:
|
|
175
|
+
"""Return state from cache."""
|
|
176
|
+
return self._get_adv_value("over_humidify_protection")
|
|
177
|
+
|
|
178
|
+
def is_tank_removed(self) -> bool | None:
|
|
179
|
+
"""Return state from cache."""
|
|
180
|
+
return self._get_adv_value("tank_removed")
|
|
181
|
+
|
|
182
|
+
def is_filter_missing(self) -> bool | None:
|
|
183
|
+
"""Return state from cache."""
|
|
184
|
+
return self._get_adv_value("filter_missing")
|
|
185
|
+
|
|
186
|
+
def is_filter_alert_on(self) -> bool | None:
|
|
187
|
+
"""Return state from cache."""
|
|
188
|
+
return self._get_adv_value("filter_alert")
|
|
189
|
+
|
|
190
|
+
def is_tilted_alert_on(self) -> bool | None:
|
|
191
|
+
"""Return state from cache."""
|
|
192
|
+
return self._get_adv_value("tilted_alert")
|
|
193
|
+
|
|
194
|
+
def get_water_level(self) -> HumidifierWaterLevel | None:
|
|
195
|
+
"""Return state from cache."""
|
|
196
|
+
return self._get_adv_value("water_level")
|
|
197
|
+
|
|
198
|
+
def get_filter_run_time(self) -> int | None:
|
|
199
|
+
"""Return state from cache."""
|
|
200
|
+
return self._get_adv_value("filter_run_time")
|
|
201
|
+
|
|
202
|
+
def get_target_humidity(self) -> int | None:
|
|
203
|
+
"""Return state from cache."""
|
|
204
|
+
return self._get_adv_value("target_humidity")
|
|
205
|
+
|
|
206
|
+
def get_humidity(self) -> int | None:
|
|
207
|
+
"""Return state from cache."""
|
|
208
|
+
return self._get_adv_value("humidity")
|
|
209
|
+
|
|
210
|
+
def get_temperature(self) -> float | None:
|
|
211
|
+
"""Return state from cache."""
|
|
212
|
+
return self._get_adv_value("temperature")
|
|
@@ -131,6 +131,12 @@ class GetSwitchbotDevices:
|
|
|
131
131
|
"""Return all WoKeypad/Keypad devices with services data."""
|
|
132
132
|
return await self._get_devices_by_model("y")
|
|
133
133
|
|
|
134
|
+
async def get_humidifiers(self) -> dict[str, SwitchBotAdvertisement]:
|
|
135
|
+
"""Return all humidifier devices with services data."""
|
|
136
|
+
humidifiers = await self._get_devices_by_model("e")
|
|
137
|
+
evaporative_humidifiers = await self._get_devices_by_model("#")
|
|
138
|
+
return {**humidifiers, **evaporative_humidifiers}
|
|
139
|
+
|
|
134
140
|
async def get_device_data(
|
|
135
141
|
self, address: str
|
|
136
142
|
) -> dict[str, SwitchBotAdvertisement] | None:
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from unittest.mock import AsyncMock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from bleak.backends.device import BLEDevice
|
|
6
|
+
|
|
7
|
+
from switchbot import SwitchBotAdvertisement, SwitchbotModel
|
|
8
|
+
from switchbot.adv_parsers.humidifier import process_evaporative_humidifier
|
|
9
|
+
from switchbot.const.evaporative_humidifier import HumidifierMode, HumidifierWaterLevel
|
|
10
|
+
from switchbot.devices import evaporative_humidifier
|
|
11
|
+
|
|
12
|
+
from .test_adv_parser import generate_ble_device
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_device_for_command_testing(init_data: dict | None = None):
|
|
16
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
17
|
+
evaporative_humidifier_device = (
|
|
18
|
+
evaporative_humidifier.SwitchbotEvaporativeHumidifier(
|
|
19
|
+
ble_device, "ff", "ffffffffffffffffffffffffffffffff"
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
evaporative_humidifier_device.update_from_advertisement(
|
|
23
|
+
make_advertisement_data(ble_device, init_data)
|
|
24
|
+
)
|
|
25
|
+
return evaporative_humidifier_device
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def make_advertisement_data(ble_device: BLEDevice, init_data: dict | None = None):
|
|
29
|
+
if init_data is None:
|
|
30
|
+
init_data = {}
|
|
31
|
+
"""Set advertisement data with defaults."""
|
|
32
|
+
return SwitchBotAdvertisement(
|
|
33
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
34
|
+
data={
|
|
35
|
+
"rawAdvData": b"#\x00\x00\x15\x1c\x00",
|
|
36
|
+
"data": {
|
|
37
|
+
"isOn": False,
|
|
38
|
+
"mode": None,
|
|
39
|
+
"target_humidity": None,
|
|
40
|
+
"child_lock": False,
|
|
41
|
+
"over_humidify_protection": True,
|
|
42
|
+
"tank_removed": False,
|
|
43
|
+
"tilted_alert": False,
|
|
44
|
+
"filter_missing": False,
|
|
45
|
+
"humidity": 51,
|
|
46
|
+
"temperature": 16.8,
|
|
47
|
+
"filter_run_time": datetime.timedelta(days=3, seconds=57600),
|
|
48
|
+
"filter_alert": False,
|
|
49
|
+
"water_level": HumidifierWaterLevel.LOW,
|
|
50
|
+
}
|
|
51
|
+
| init_data,
|
|
52
|
+
"isEncrypted": False,
|
|
53
|
+
"model": "#",
|
|
54
|
+
"modelFriendlyName": "Evaporative Humidifier",
|
|
55
|
+
"modelName": SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
|
|
56
|
+
},
|
|
57
|
+
device=ble_device,
|
|
58
|
+
rssi=-80,
|
|
59
|
+
active=True,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.asyncio
|
|
64
|
+
async def test_process_advertisement():
|
|
65
|
+
data = process_evaporative_humidifier(
|
|
66
|
+
b"#\x00\x00\x15\x1c\x00",
|
|
67
|
+
b"\xd4\x8cIU\x95\xb2\x08\x06\x88\xb3\x90\x81\x00X\x00X2",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert data == {
|
|
71
|
+
"isOn": False,
|
|
72
|
+
"mode": None,
|
|
73
|
+
"target_humidity": None,
|
|
74
|
+
"child_lock": False,
|
|
75
|
+
"over_humidify_protection": None,
|
|
76
|
+
"tank_removed": False,
|
|
77
|
+
"tilted_alert": False,
|
|
78
|
+
"filter_missing": False,
|
|
79
|
+
"humidity": 51,
|
|
80
|
+
"temperature": 16.8,
|
|
81
|
+
"filter_run_time": datetime.timedelta(days=3, seconds=57600),
|
|
82
|
+
"filter_alert": False,
|
|
83
|
+
"water_level": HumidifierWaterLevel.LOW,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.mark.asyncio
|
|
88
|
+
async def test_process_advertisement_empty():
|
|
89
|
+
data = process_evaporative_humidifier(None, None)
|
|
90
|
+
|
|
91
|
+
assert data == {
|
|
92
|
+
"isOn": None,
|
|
93
|
+
"mode": None,
|
|
94
|
+
"target_humidity": None,
|
|
95
|
+
"child_lock": None,
|
|
96
|
+
"over_humidify_protection": None,
|
|
97
|
+
"tank_removed": None,
|
|
98
|
+
"tilted_alert": None,
|
|
99
|
+
"filter_missing": None,
|
|
100
|
+
"humidity": None,
|
|
101
|
+
"temperature": None,
|
|
102
|
+
"filter_run_time": None,
|
|
103
|
+
"filter_alert": None,
|
|
104
|
+
"water_level": None,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_turn_on():
|
|
110
|
+
device = create_device_for_command_testing({"isOn": False})
|
|
111
|
+
device._send_command = AsyncMock(return_value=b"\x01")
|
|
112
|
+
|
|
113
|
+
assert device.is_on() is False
|
|
114
|
+
await device.turn_on()
|
|
115
|
+
assert device.is_on() is True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.asyncio
|
|
119
|
+
async def test_turn_off():
|
|
120
|
+
device = create_device_for_command_testing({"isOn": True})
|
|
121
|
+
device._send_command = AsyncMock(return_value=b"\x01")
|
|
122
|
+
|
|
123
|
+
assert device.is_on() is True
|
|
124
|
+
await device.turn_off()
|
|
125
|
+
assert device.is_on() is False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@pytest.mark.asyncio
|
|
129
|
+
async def test_set_mode():
|
|
130
|
+
device = create_device_for_command_testing(
|
|
131
|
+
{"isOn": True, "mode": HumidifierMode.LOW}
|
|
132
|
+
)
|
|
133
|
+
device._send_command = AsyncMock(return_value=b"\x01")
|
|
134
|
+
|
|
135
|
+
assert device.get_mode() is HumidifierMode.LOW
|
|
136
|
+
await device.set_mode(HumidifierMode.AUTO)
|
|
137
|
+
assert device.get_mode() is HumidifierMode.AUTO
|
|
138
|
+
|
|
139
|
+
await device.set_mode(HumidifierMode.TARGET_HUMIDITY, 60)
|
|
140
|
+
assert device.get_mode() is HumidifierMode.TARGET_HUMIDITY
|
|
141
|
+
assert device.get_target_humidity() == 60
|
|
142
|
+
|
|
143
|
+
await device.set_mode(HumidifierMode.DRYING_FILTER)
|
|
144
|
+
assert device.get_mode() is HumidifierMode.DRYING_FILTER
|
|
145
|
+
|
|
146
|
+
with pytest.raises(ValueError):
|
|
147
|
+
await device.set_mode(0)
|
|
148
|
+
|
|
149
|
+
with pytest.raises(TypeError):
|
|
150
|
+
await device.set_mode(HumidifierMode.TARGET_HUMIDITY)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.asyncio
|
|
154
|
+
async def test_set_child_lock():
|
|
155
|
+
device = create_device_for_command_testing({"child_lock": False})
|
|
156
|
+
device._send_command = AsyncMock(return_value=b"\x01")
|
|
157
|
+
|
|
158
|
+
assert device.is_child_lock_enabled() is False
|
|
159
|
+
await device.set_child_lock(True)
|
|
160
|
+
assert device.is_child_lock_enabled() is True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.asyncio
|
|
164
|
+
async def test_start_drying_filter():
|
|
165
|
+
device = create_device_for_command_testing(
|
|
166
|
+
{"isOn": True, "mode": HumidifierMode.AUTO}
|
|
167
|
+
)
|
|
168
|
+
device._send_command = AsyncMock(return_value=b"\x01")
|
|
169
|
+
|
|
170
|
+
assert device.get_mode() is HumidifierMode.AUTO
|
|
171
|
+
await device.start_drying_filter()
|
|
172
|
+
assert device.get_mode() is HumidifierMode.DRYING_FILTER
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.asyncio
|
|
176
|
+
async def test_stop_drying_filter():
|
|
177
|
+
device = create_device_for_command_testing(
|
|
178
|
+
{"isOn": True, "mode": HumidifierMode.DRYING_FILTER}
|
|
179
|
+
)
|
|
180
|
+
device._send_command = AsyncMock(return_value=b"\x00")
|
|
181
|
+
|
|
182
|
+
assert device.is_on() is True
|
|
183
|
+
assert device.get_mode() is HumidifierMode.DRYING_FILTER
|
|
184
|
+
await device.stop_drying_filter()
|
|
185
|
+
assert device.is_on() is False
|
|
186
|
+
assert device.get_mode() is None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.asyncio
|
|
190
|
+
async def test_attributes():
|
|
191
|
+
device = create_device_for_command_testing()
|
|
192
|
+
device._send_command = AsyncMock(return_value=b"\x01")
|
|
193
|
+
|
|
194
|
+
assert device.is_over_humidify_protection_enabled() is True
|
|
195
|
+
assert device.is_tank_removed() is False
|
|
196
|
+
assert device.is_filter_missing() is False
|
|
197
|
+
assert device.is_filter_alert_on() is False
|
|
198
|
+
assert device.is_tilted_alert_on() is False
|
|
199
|
+
assert device.get_water_level() is HumidifierWaterLevel.LOW
|
|
200
|
+
assert device.get_filter_run_time() == datetime.timedelta(days=3, seconds=57600)
|
|
201
|
+
assert device.get_humidity() == 51
|
|
202
|
+
assert device.get_temperature() == 16.8
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""Humidifier adv parser."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
|
|
7
|
-
_LOGGER = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
# mfr_data: 943cc68d3d2e
|
|
10
|
-
# data: 650000cd802b6300
|
|
11
|
-
# data: 650000cd802b6300
|
|
12
|
-
# data: 658000c9802b6300
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# Low: 658000c5222b6300
|
|
16
|
-
# Med: 658000c5432b6300
|
|
17
|
-
# High: 658000c5642b6300
|
|
18
|
-
def process_wohumidifier(
|
|
19
|
-
data: bytes | None, mfr_data: bytes | None
|
|
20
|
-
) -> dict[str, bool | int]:
|
|
21
|
-
"""Process WoHumi services data."""
|
|
22
|
-
if data is None:
|
|
23
|
-
return {
|
|
24
|
-
"isOn": None,
|
|
25
|
-
"level": None,
|
|
26
|
-
"switchMode": True,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
"isOn": bool(data[1]),
|
|
31
|
-
"level": data[4],
|
|
32
|
-
"switchMode": True,
|
|
33
|
-
}
|
|
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
|