PySwitchbot 0.65.0__tar.gz → 0.67.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.65.0 → pyswitchbot-0.67.0}/PKG-INFO +1 -1
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/SOURCES.txt +6 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/setup.py +1 -1
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/__init__.py +10 -2
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parser.py +20 -3
- pyswitchbot-0.67.0/switchbot/adv_parsers/light_strip.py +32 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/lock.py +17 -4
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/__init__.py +12 -0
- pyswitchbot-0.67.0/switchbot/const/light.py +34 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/air_purifier.py +3 -14
- pyswitchbot-0.67.0/switchbot/devices/base_light.py +179 -0
- pyswitchbot-0.67.0/switchbot/devices/bulb.py +66 -0
- pyswitchbot-0.67.0/switchbot/devices/ceiling_light.py +69 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/device.py +133 -69
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/evaporative_humidifier.py +3 -13
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/fan.py +2 -14
- pyswitchbot-0.67.0/switchbot/devices/light_strip.py +179 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/lock.py +30 -12
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/relay_switch.py +5 -15
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/vacuum.py +0 -3
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_adv_parser.py +184 -8
- pyswitchbot-0.67.0/tests/test_bulb.py +251 -0
- pyswitchbot-0.67.0/tests/test_ceiling_light.py +185 -0
- pyswitchbot-0.67.0/tests/test_colormode_imports.py +88 -0
- pyswitchbot-0.67.0/tests/test_encrypted_device.py +367 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_fan.py +8 -5
- pyswitchbot-0.67.0/tests/test_lock.py +677 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_relay_switch.py +2 -2
- pyswitchbot-0.67.0/tests/test_strip_light.py +359 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_vacuum.py +4 -2
- pyswitchbot-0.65.0/switchbot/adv_parsers/light_strip.py +0 -21
- pyswitchbot-0.65.0/switchbot/devices/base_light.py +0 -109
- pyswitchbot-0.65.0/switchbot/devices/bulb.py +0 -94
- pyswitchbot-0.65.0/switchbot/devices/ceiling_light.py +0 -69
- pyswitchbot-0.65.0/switchbot/devices/light_strip.py +0 -84
- pyswitchbot-0.65.0/tests/test_lock.py +0 -42
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/LICENSE +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/README.md +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/pyproject.toml +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/setup.cfg +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/enum.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_air_purifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_helpers.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_hub3.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_roller_shade.py +0 -0
|
@@ -45,6 +45,7 @@ switchbot/const/evaporative_humidifier.py
|
|
|
45
45
|
switchbot/const/fan.py
|
|
46
46
|
switchbot/const/hub2.py
|
|
47
47
|
switchbot/const/hub3.py
|
|
48
|
+
switchbot/const/light.py
|
|
48
49
|
switchbot/const/lock.py
|
|
49
50
|
switchbot/devices/__init__.py
|
|
50
51
|
switchbot/devices/air_purifier.py
|
|
@@ -73,7 +74,11 @@ tests/test_adv_parser.py
|
|
|
73
74
|
tests/test_air_purifier.py
|
|
74
75
|
tests/test_base_cover.py
|
|
75
76
|
tests/test_blind_tilt.py
|
|
77
|
+
tests/test_bulb.py
|
|
78
|
+
tests/test_ceiling_light.py
|
|
79
|
+
tests/test_colormode_imports.py
|
|
76
80
|
tests/test_curtain.py
|
|
81
|
+
tests/test_encrypted_device.py
|
|
77
82
|
tests/test_evaporative_humidifier.py
|
|
78
83
|
tests/test_fan.py
|
|
79
84
|
tests/test_helpers.py
|
|
@@ -82,4 +87,5 @@ tests/test_hub3.py
|
|
|
82
87
|
tests/test_lock.py
|
|
83
88
|
tests/test_relay_switch.py
|
|
84
89
|
tests/test_roller_shade.py
|
|
90
|
+
tests/test_strip_light.py
|
|
85
91
|
tests/test_vacuum.py
|
|
@@ -11,11 +11,15 @@ from bleak_retry_connector import (
|
|
|
11
11
|
from .adv_parser import SwitchbotSupportedType, parse_advertisement_data
|
|
12
12
|
from .const import (
|
|
13
13
|
AirPurifierMode,
|
|
14
|
+
BulbColorMode,
|
|
15
|
+
CeilingLightColorMode,
|
|
16
|
+
ColorMode,
|
|
14
17
|
FanMode,
|
|
15
18
|
HumidifierAction,
|
|
16
19
|
HumidifierMode,
|
|
17
20
|
HumidifierWaterLevel,
|
|
18
21
|
LockStatus,
|
|
22
|
+
StripLightColorMode,
|
|
19
23
|
SwitchbotAccountConnectionError,
|
|
20
24
|
SwitchbotApiError,
|
|
21
25
|
SwitchbotAuthenticationError,
|
|
@@ -28,11 +32,11 @@ from .devices.bot import Switchbot
|
|
|
28
32
|
from .devices.bulb import SwitchbotBulb
|
|
29
33
|
from .devices.ceiling_light import SwitchbotCeilingLight
|
|
30
34
|
from .devices.curtain import SwitchbotCurtain
|
|
31
|
-
from .devices.device import
|
|
35
|
+
from .devices.device import SwitchbotDevice, SwitchbotEncryptedDevice
|
|
32
36
|
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
|
|
33
37
|
from .devices.fan import SwitchbotFan
|
|
34
38
|
from .devices.humidifier import SwitchbotHumidifier
|
|
35
|
-
from .devices.light_strip import SwitchbotLightStrip
|
|
39
|
+
from .devices.light_strip import SwitchbotLightStrip, SwitchbotStripLight3
|
|
36
40
|
from .devices.lock import SwitchbotLock
|
|
37
41
|
from .devices.plug import SwitchbotPlugMini
|
|
38
42
|
from .devices.relay_switch import SwitchbotRelaySwitch, SwitchbotRelaySwitch2PM
|
|
@@ -43,6 +47,8 @@ from .models import SwitchBotAdvertisement
|
|
|
43
47
|
|
|
44
48
|
__all__ = [
|
|
45
49
|
"AirPurifierMode",
|
|
50
|
+
"BulbColorMode",
|
|
51
|
+
"CeilingLightColorMode",
|
|
46
52
|
"ColorMode",
|
|
47
53
|
"FanMode",
|
|
48
54
|
"GetSwitchbotDevices",
|
|
@@ -50,6 +56,7 @@ __all__ = [
|
|
|
50
56
|
"HumidifierMode",
|
|
51
57
|
"HumidifierWaterLevel",
|
|
52
58
|
"LockStatus",
|
|
59
|
+
"StripLightColorMode",
|
|
53
60
|
"SwitchBotAdvertisement",
|
|
54
61
|
"Switchbot",
|
|
55
62
|
"Switchbot",
|
|
@@ -76,6 +83,7 @@ __all__ = [
|
|
|
76
83
|
"SwitchbotRelaySwitch",
|
|
77
84
|
"SwitchbotRelaySwitch2PM",
|
|
78
85
|
"SwitchbotRollerShade",
|
|
86
|
+
"SwitchbotStripLight3",
|
|
79
87
|
"SwitchbotSupportedType",
|
|
80
88
|
"SwitchbotSupportedType",
|
|
81
89
|
"SwitchbotVacuum",
|
|
@@ -24,8 +24,13 @@ from .adv_parsers.hubmini_matter import process_hubmini_matter
|
|
|
24
24
|
from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
|
|
25
25
|
from .adv_parsers.keypad import process_wokeypad
|
|
26
26
|
from .adv_parsers.leak import process_leak
|
|
27
|
-
from .adv_parsers.light_strip import process_wostrip
|
|
28
|
-
from .adv_parsers.lock import
|
|
27
|
+
from .adv_parsers.light_strip import process_light, process_wostrip
|
|
28
|
+
from .adv_parsers.lock import (
|
|
29
|
+
process_lock2,
|
|
30
|
+
process_locklite,
|
|
31
|
+
process_wolock,
|
|
32
|
+
process_wolock_pro,
|
|
33
|
+
)
|
|
29
34
|
from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
|
|
30
35
|
from .adv_parsers.motion import process_wopresence
|
|
31
36
|
from .adv_parsers.plug import process_woplugmini
|
|
@@ -304,7 +309,7 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
304
309
|
"-": {
|
|
305
310
|
"modelName": SwitchbotModel.LOCK_LITE,
|
|
306
311
|
"modelFriendlyName": "Lock Lite",
|
|
307
|
-
"func":
|
|
312
|
+
"func": process_locklite,
|
|
308
313
|
"manufacturer_id": 2409,
|
|
309
314
|
},
|
|
310
315
|
b"\x00\x10\xa5\xb8": {
|
|
@@ -325,6 +330,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
325
330
|
"func": process_relay_switch_2pm,
|
|
326
331
|
"manufacturer_id": 2409,
|
|
327
332
|
},
|
|
333
|
+
b"\x00\x10\xd0\xb0": {
|
|
334
|
+
"modelName": SwitchbotModel.FLOOR_LAMP,
|
|
335
|
+
"modelFriendlyName": "Floor Lamp",
|
|
336
|
+
"func": process_light,
|
|
337
|
+
"manufacturer_id": 2409,
|
|
338
|
+
},
|
|
339
|
+
b"\x00\x10\xd0\xb1": {
|
|
340
|
+
"modelName": SwitchbotModel.STRIP_LIGHT_3,
|
|
341
|
+
"modelFriendlyName": "Strip Light 3",
|
|
342
|
+
"func": process_light,
|
|
343
|
+
"manufacturer_id": 2409,
|
|
344
|
+
},
|
|
328
345
|
}
|
|
329
346
|
|
|
330
347
|
_SWITCHBOT_MODEL_TO_CHAR = {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Light strip adv parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def process_wostrip(
|
|
9
|
+
data: bytes | None, mfr_data: bytes | None
|
|
10
|
+
) -> dict[str, bool | int]:
|
|
11
|
+
"""Process WoStrip services data."""
|
|
12
|
+
if mfr_data is None:
|
|
13
|
+
return {}
|
|
14
|
+
return {
|
|
15
|
+
"sequence_number": mfr_data[6],
|
|
16
|
+
"isOn": bool(mfr_data[7] & 0b10000000),
|
|
17
|
+
"brightness": mfr_data[7] & 0b01111111,
|
|
18
|
+
"delay": bool(mfr_data[8] & 0b10000000),
|
|
19
|
+
"network_state": (mfr_data[8] & 0b01110000) >> 4,
|
|
20
|
+
"color_mode": mfr_data[8] & 0b00001111,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def process_light(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
25
|
+
"""Support for strip light 3 and floor lamp."""
|
|
26
|
+
common_data = process_wostrip(data, mfr_data)
|
|
27
|
+
if not common_data:
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
light_data = {"cw": struct.unpack(">H", mfr_data[16:18])[0]}
|
|
31
|
+
|
|
32
|
+
return common_data | light_data
|
|
@@ -11,6 +11,21 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
11
11
|
|
|
12
12
|
def process_wolock(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
13
13
|
"""Support for lock and lock lite process data."""
|
|
14
|
+
common_data = process_locklite(data, mfr_data)
|
|
15
|
+
if not common_data:
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
common_data["door_open"] = bool(mfr_data[7] & 0b00000100)
|
|
19
|
+
common_data["unclosed_alarm"] = bool(mfr_data[8] & 0b00100000)
|
|
20
|
+
common_data["auto_lock_paused"] = bool(mfr_data[8] & 0b00000010)
|
|
21
|
+
|
|
22
|
+
return common_data
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def process_locklite(
|
|
26
|
+
data: bytes | None, mfr_data: bytes | None
|
|
27
|
+
) -> dict[str, bool | int]:
|
|
28
|
+
"""Support for lock lite process data."""
|
|
14
29
|
if mfr_data is None:
|
|
15
30
|
return {}
|
|
16
31
|
|
|
@@ -24,11 +39,8 @@ def process_wolock(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool
|
|
|
24
39
|
"calibration": bool(mfr_data[7] & 0b10000000),
|
|
25
40
|
"status": LockStatus((mfr_data[7] & 0b01110000) >> 4),
|
|
26
41
|
"update_from_secondary_lock": bool(mfr_data[7] & 0b00001000),
|
|
27
|
-
"door_open": bool(mfr_data[7] & 0b00000100),
|
|
28
42
|
"double_lock_mode": bool(mfr_data[8] & 0b10000000),
|
|
29
|
-
"unclosed_alarm": bool(mfr_data[8] & 0b00100000),
|
|
30
43
|
"unlocked_alarm": bool(mfr_data[8] & 0b00010000),
|
|
31
|
-
"auto_lock_paused": bool(mfr_data[8] & 0b00000010),
|
|
32
44
|
"night_latch": bool(mfr_data[9] & 0b00000001) if len(mfr_data) > 9 else False,
|
|
33
45
|
}
|
|
34
46
|
|
|
@@ -37,10 +49,11 @@ def parse_common_data(mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
|
37
49
|
if mfr_data is None:
|
|
38
50
|
return {}
|
|
39
51
|
|
|
52
|
+
_LOGGER.debug("mfr_data: %s", mfr_data.hex())
|
|
40
53
|
return {
|
|
41
54
|
"sequence_number": mfr_data[6],
|
|
42
55
|
"calibration": bool(mfr_data[7] & 0b10000000),
|
|
43
|
-
"status": LockStatus((mfr_data[7] & 0b01111000) >>
|
|
56
|
+
"status": LockStatus((mfr_data[7] & 0b01111000) >> 3),
|
|
44
57
|
"update_from_secondary_lock": bool(mfr_data[8] & 0b11000000),
|
|
45
58
|
"door_open_from_secondary_lock": bool(mfr_data[8] & 0b00100000),
|
|
46
59
|
"door_open": bool(mfr_data[8] & 0b00010000),
|
|
@@ -10,6 +10,12 @@ from .evaporative_humidifier import (
|
|
|
10
10
|
HumidifierWaterLevel,
|
|
11
11
|
)
|
|
12
12
|
from .fan import FanMode
|
|
13
|
+
from .light import (
|
|
14
|
+
BulbColorMode,
|
|
15
|
+
CeilingLightColorMode,
|
|
16
|
+
ColorMode,
|
|
17
|
+
StripLightColorMode,
|
|
18
|
+
)
|
|
13
19
|
|
|
14
20
|
# Preserve old LockStatus export for backwards compatibility
|
|
15
21
|
from .lock import LockStatus
|
|
@@ -85,6 +91,8 @@ class SwitchbotModel(StrEnum):
|
|
|
85
91
|
LOCK_LITE = "Lock Lite"
|
|
86
92
|
GARAGE_DOOR_OPENER = "Garage Door Opener"
|
|
87
93
|
RELAY_SWITCH_2PM = "Relay Switch 2PM"
|
|
94
|
+
STRIP_LIGHT_3 = "Strip Light 3"
|
|
95
|
+
FLOOR_LAMP = "Floor Lamp"
|
|
88
96
|
|
|
89
97
|
|
|
90
98
|
__all__ = [
|
|
@@ -92,11 +100,15 @@ __all__ = [
|
|
|
92
100
|
"DEFAULT_RETRY_TIMEOUT",
|
|
93
101
|
"DEFAULT_SCAN_TIMEOUT",
|
|
94
102
|
"AirPurifierMode",
|
|
103
|
+
"BulbColorMode",
|
|
104
|
+
"CeilingLightColorMode",
|
|
105
|
+
"ColorMode",
|
|
95
106
|
"FanMode",
|
|
96
107
|
"HumidifierAction",
|
|
97
108
|
"HumidifierMode",
|
|
98
109
|
"HumidifierWaterLevel",
|
|
99
110
|
"LockStatus",
|
|
111
|
+
"StripLightColorMode",
|
|
100
112
|
"SwitchbotAccountConnectionError",
|
|
101
113
|
"SwitchbotApiError",
|
|
102
114
|
"SwitchbotAuthenticationError",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ColorMode(Enum):
|
|
5
|
+
OFF = 0
|
|
6
|
+
COLOR_TEMP = 1
|
|
7
|
+
RGB = 2
|
|
8
|
+
EFFECT = 3
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StripLightColorMode(Enum):
|
|
12
|
+
RGB = 2
|
|
13
|
+
SCENE = 3
|
|
14
|
+
MUSIC = 4
|
|
15
|
+
CONTROLLER = 5
|
|
16
|
+
COLOR_TEMP = 6
|
|
17
|
+
UNKNOWN = 10
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BulbColorMode(Enum):
|
|
21
|
+
COLOR_TEMP = 1
|
|
22
|
+
RGB = 2
|
|
23
|
+
DYNAMIC = 3
|
|
24
|
+
UNKNOWN = 10
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CeilingLightColorMode(Enum):
|
|
28
|
+
COLOR_TEMP = 0
|
|
29
|
+
NIGHT = 1
|
|
30
|
+
MUSIC = 4
|
|
31
|
+
UNKNOWN = 10
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEFAULT_COLOR_TEMP = 4001
|
|
@@ -21,8 +21,6 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
COMMAND_HEAD = "570f4c"
|
|
24
|
-
COMMAND_TURN_OFF = f"{COMMAND_HEAD}010000"
|
|
25
|
-
COMMAND_TURN_ON = f"{COMMAND_HEAD}010100"
|
|
26
24
|
COMMAND_SET_MODE = {
|
|
27
25
|
AirPurifierMode.LEVEL_1.name.lower(): f"{COMMAND_HEAD}01010100",
|
|
28
26
|
AirPurifierMode.LEVEL_2.name.lower(): f"{COMMAND_HEAD}01010132",
|
|
@@ -37,6 +35,9 @@ DEVICE_GET_BASIC_SETTINGS_KEY = "570f4d81"
|
|
|
37
35
|
class SwitchbotAirPurifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
38
36
|
"""Representation of a Switchbot Air Purifier."""
|
|
39
37
|
|
|
38
|
+
_turn_on_command = f"{COMMAND_HEAD}010100"
|
|
39
|
+
_turn_off_command = f"{COMMAND_HEAD}010000"
|
|
40
|
+
|
|
40
41
|
def __init__(
|
|
41
42
|
self,
|
|
42
43
|
device: BLEDevice,
|
|
@@ -109,18 +110,6 @@ class SwitchbotAirPurifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
109
110
|
result = await self._send_command(COMMAND_SET_MODE[preset_mode])
|
|
110
111
|
return self._check_command_result(result, 0, {1})
|
|
111
112
|
|
|
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
113
|
def get_current_percentage(self) -> Any:
|
|
125
114
|
"""Return cached percentage."""
|
|
126
115
|
return self._get_adv_value("speed")
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..helpers import create_background_task
|
|
8
|
+
from ..models import SwitchBotAdvertisement
|
|
9
|
+
from .device import SwitchbotDevice, SwitchbotOperationError, update_after_operation
|
|
10
|
+
|
|
11
|
+
_LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SwitchbotBaseLight(SwitchbotDevice):
|
|
15
|
+
"""Representation of a Switchbot light."""
|
|
16
|
+
|
|
17
|
+
_effect_dict: dict[str, list[str]] = {}
|
|
18
|
+
_set_brightness_command: str = ""
|
|
19
|
+
_set_color_temp_command: str = ""
|
|
20
|
+
_set_rgb_command: str = ""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
23
|
+
"""Switchbot base light constructor."""
|
|
24
|
+
super().__init__(*args, **kwargs)
|
|
25
|
+
self._state: dict[str, Any] = {}
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def on(self) -> bool | None:
|
|
29
|
+
"""Return if light is on."""
|
|
30
|
+
return self.is_on()
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def rgb(self) -> tuple[int, int, int] | None:
|
|
34
|
+
"""Return the current rgb value."""
|
|
35
|
+
if "r" not in self._state or "g" not in self._state or "b" not in self._state:
|
|
36
|
+
return None
|
|
37
|
+
return self._state["r"], self._state["g"], self._state["b"]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def color_temp(self) -> int | None:
|
|
41
|
+
"""Return the current color temp value."""
|
|
42
|
+
return self._state.get("cw") or self.min_temp
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def brightness(self) -> int | None:
|
|
46
|
+
"""Return the current brightness value."""
|
|
47
|
+
return self._get_adv_value("brightness") or 0
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def color_mode(self) -> Any:
|
|
52
|
+
"""Return the current color mode."""
|
|
53
|
+
raise NotImplementedError("Subclasses must implement color mode")
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def min_temp(self) -> int:
|
|
57
|
+
"""Return minimum color temp."""
|
|
58
|
+
return 2700
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def max_temp(self) -> int:
|
|
62
|
+
"""Return maximum color temp."""
|
|
63
|
+
return 6500
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def get_effect_list(self) -> list[str] | None:
|
|
67
|
+
"""Return the list of supported effects."""
|
|
68
|
+
return list(self._effect_dict) if self._effect_dict else None
|
|
69
|
+
|
|
70
|
+
def is_on(self) -> bool | None:
|
|
71
|
+
"""Return bulb state from cache."""
|
|
72
|
+
return self._get_adv_value("isOn")
|
|
73
|
+
|
|
74
|
+
def get_effect(self):
|
|
75
|
+
"""Return the current effect."""
|
|
76
|
+
return self._get_adv_value("effect")
|
|
77
|
+
|
|
78
|
+
@update_after_operation
|
|
79
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
80
|
+
"""Set brightness."""
|
|
81
|
+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
|
|
82
|
+
hex_brightness = f"{brightness:02X}"
|
|
83
|
+
self._check_function_support(self._set_brightness_command)
|
|
84
|
+
result = await self._send_command(
|
|
85
|
+
self._set_brightness_command.format(hex_brightness)
|
|
86
|
+
)
|
|
87
|
+
return self._check_command_result(result, 0, {1})
|
|
88
|
+
|
|
89
|
+
@update_after_operation
|
|
90
|
+
async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
|
|
91
|
+
"""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"
|
|
94
|
+
hex_data = f"{brightness:02X}{color_temp:04X}"
|
|
95
|
+
self._check_function_support(self._set_color_temp_command)
|
|
96
|
+
result = await self._send_command(self._set_color_temp_command.format(hex_data))
|
|
97
|
+
return self._check_command_result(result, 0, {1})
|
|
98
|
+
|
|
99
|
+
@update_after_operation
|
|
100
|
+
async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
|
|
101
|
+
"""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"
|
|
106
|
+
self._check_function_support(self._set_rgb_command)
|
|
107
|
+
hex_data = f"{brightness:02X}{r:02X}{g:02X}{b:02X}"
|
|
108
|
+
result = await self._send_command(self._set_rgb_command.format(hex_data))
|
|
109
|
+
return self._check_command_result(result, 0, {1})
|
|
110
|
+
|
|
111
|
+
@update_after_operation
|
|
112
|
+
async def set_effect(self, effect: str) -> bool:
|
|
113
|
+
"""Set effect."""
|
|
114
|
+
effect_template = self._effect_dict.get(effect.lower())
|
|
115
|
+
if not effect_template:
|
|
116
|
+
raise SwitchbotOperationError(f"Effect {effect} not supported")
|
|
117
|
+
result = await self._send_multiple_commands(effect_template)
|
|
118
|
+
if result:
|
|
119
|
+
self._override_state({"effect": effect})
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Send multiple commands to device.
|
|
125
|
+
|
|
126
|
+
Since we current have no way to tell which command the device
|
|
127
|
+
needs we send both.
|
|
128
|
+
"""
|
|
129
|
+
final_result = False
|
|
130
|
+
for key in keys:
|
|
131
|
+
result = await self._send_command(key)
|
|
132
|
+
final_result |= self._check_command_result(result, 0, {1})
|
|
133
|
+
return final_result
|
|
134
|
+
|
|
135
|
+
async def _get_multi_commands_results(
|
|
136
|
+
self, commands: list[str]
|
|
137
|
+
) -> tuple[bytes, bytes] | None:
|
|
138
|
+
"""Check results after sending multiple commands."""
|
|
139
|
+
if not (results := await self._get_basic_info_by_multi_commands(commands)):
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
_version_info, _data = results[0], results[1]
|
|
143
|
+
_LOGGER.debug(
|
|
144
|
+
"version info: %s, data: %s, address: %s",
|
|
145
|
+
_version_info,
|
|
146
|
+
_data,
|
|
147
|
+
self._device.address,
|
|
148
|
+
)
|
|
149
|
+
return _version_info, _data
|
|
150
|
+
|
|
151
|
+
async def _get_basic_info_by_multi_commands(
|
|
152
|
+
self, commands: list[str]
|
|
153
|
+
) -> list[bytes] | None:
|
|
154
|
+
"""Get device basic settings by sending multiple commands."""
|
|
155
|
+
results = []
|
|
156
|
+
for command in commands:
|
|
157
|
+
if not (result := await self._get_basic_info(command)):
|
|
158
|
+
return None
|
|
159
|
+
results.append(result)
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
|
|
164
|
+
"""Representation of a Switchbot light."""
|
|
165
|
+
|
|
166
|
+
def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
|
|
167
|
+
"""Update device data from advertisement."""
|
|
168
|
+
current_state = self._get_adv_value("sequence_number")
|
|
169
|
+
super().update_from_advertisement(advertisement)
|
|
170
|
+
new_state = self._get_adv_value("sequence_number")
|
|
171
|
+
_LOGGER.debug(
|
|
172
|
+
"%s: update advertisement: %s (seq before: %s) (seq after: %s)",
|
|
173
|
+
self.name,
|
|
174
|
+
advertisement,
|
|
175
|
+
current_state,
|
|
176
|
+
new_state,
|
|
177
|
+
)
|
|
178
|
+
if current_state != new_state:
|
|
179
|
+
create_background_task(self.update())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..const.light import BulbColorMode, ColorMode
|
|
6
|
+
from .base_light import SwitchbotSequenceBaseLight
|
|
7
|
+
|
|
8
|
+
# Private mapping from device-specific color modes to original ColorMode enum
|
|
9
|
+
_BULB_COLOR_MODE_MAP = {
|
|
10
|
+
BulbColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
|
11
|
+
BulbColorMode.RGB: ColorMode.RGB,
|
|
12
|
+
BulbColorMode.DYNAMIC: ColorMode.EFFECT,
|
|
13
|
+
BulbColorMode.UNKNOWN: ColorMode.OFF,
|
|
14
|
+
}
|
|
15
|
+
COLOR_BULB_CONTROL_HEADER = "570F4701"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SwitchbotBulb(SwitchbotSequenceBaseLight):
|
|
19
|
+
"""Representation of a Switchbot bulb."""
|
|
20
|
+
|
|
21
|
+
_turn_on_command = f"{COLOR_BULB_CONTROL_HEADER}01"
|
|
22
|
+
_turn_off_command = f"{COLOR_BULB_CONTROL_HEADER}02"
|
|
23
|
+
_set_rgb_command = f"{COLOR_BULB_CONTROL_HEADER}12{{}}"
|
|
24
|
+
_set_color_temp_command = f"{COLOR_BULB_CONTROL_HEADER}13{{}}"
|
|
25
|
+
_set_brightness_command = f"{COLOR_BULB_CONTROL_HEADER}14{{}}"
|
|
26
|
+
_get_basic_info_command = ["570003", "570f4801"]
|
|
27
|
+
_effect_dict = {
|
|
28
|
+
"colorful": ["570F4701010300"],
|
|
29
|
+
"flickering": ["570F4701010301"],
|
|
30
|
+
"breathing": ["570F4701010302"],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def color_modes(self) -> set[ColorMode]:
|
|
35
|
+
"""Return the supported color modes."""
|
|
36
|
+
return {ColorMode.RGB, ColorMode.COLOR_TEMP}
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def color_mode(self) -> ColorMode:
|
|
40
|
+
"""Return the current color mode."""
|
|
41
|
+
device_mode = BulbColorMode(self._get_adv_value("color_mode") or 10)
|
|
42
|
+
return _BULB_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
|
|
43
|
+
|
|
44
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
45
|
+
"""Get device basic settings."""
|
|
46
|
+
if not (
|
|
47
|
+
res := await self._get_multi_commands_results(self._get_basic_info_command)
|
|
48
|
+
):
|
|
49
|
+
return None
|
|
50
|
+
_version_info, _data = res
|
|
51
|
+
|
|
52
|
+
self._state["r"] = _data[3]
|
|
53
|
+
self._state["g"] = _data[4]
|
|
54
|
+
self._state["b"] = _data[5]
|
|
55
|
+
self._state["cw"] = int.from_bytes(_data[6:8], "big")
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
"isOn": bool(_data[1] & 0b10000000),
|
|
59
|
+
"brightness": _data[2] & 0b01111111,
|
|
60
|
+
"r": self._state["r"],
|
|
61
|
+
"g": self._state["g"],
|
|
62
|
+
"b": self._state["b"],
|
|
63
|
+
"cw": self._state["cw"],
|
|
64
|
+
"color_mode": _data[10] & 0b00001111,
|
|
65
|
+
"firmware": _version_info[2] / 10.0,
|
|
66
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..const.light import (
|
|
6
|
+
DEFAULT_COLOR_TEMP,
|
|
7
|
+
CeilingLightColorMode,
|
|
8
|
+
ColorMode,
|
|
9
|
+
)
|
|
10
|
+
from .base_light import SwitchbotSequenceBaseLight
|
|
11
|
+
from .device import update_after_operation
|
|
12
|
+
|
|
13
|
+
# Private mapping from device-specific color modes to original ColorMode enum
|
|
14
|
+
_CEILING_LIGHT_COLOR_MODE_MAP = {
|
|
15
|
+
CeilingLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
|
16
|
+
CeilingLightColorMode.NIGHT: ColorMode.COLOR_TEMP,
|
|
17
|
+
CeilingLightColorMode.MUSIC: ColorMode.EFFECT,
|
|
18
|
+
CeilingLightColorMode.UNKNOWN: ColorMode.OFF,
|
|
19
|
+
}
|
|
20
|
+
CEILING_LIGHT_CONTROL_HEADER = "570F5401"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SwitchbotCeilingLight(SwitchbotSequenceBaseLight):
|
|
24
|
+
"""Representation of a Switchbot ceiling light."""
|
|
25
|
+
|
|
26
|
+
_turn_on_command = f"{CEILING_LIGHT_CONTROL_HEADER}01FF01FFFF"
|
|
27
|
+
_turn_off_command = f"{CEILING_LIGHT_CONTROL_HEADER}02FF01FFFF"
|
|
28
|
+
_set_brightness_command = f"{CEILING_LIGHT_CONTROL_HEADER}01FF01{{}}"
|
|
29
|
+
_set_color_temp_command = f"{CEILING_LIGHT_CONTROL_HEADER}01FF01{{}}"
|
|
30
|
+
_get_basic_info_command = ["5702", "570f5581"]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def color_modes(self) -> set[ColorMode]:
|
|
34
|
+
"""Return the supported color modes."""
|
|
35
|
+
return {ColorMode.COLOR_TEMP}
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def color_mode(self) -> ColorMode:
|
|
39
|
+
"""Return the current color mode."""
|
|
40
|
+
device_mode = CeilingLightColorMode(self._get_adv_value("color_mode") or 10)
|
|
41
|
+
return _CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
|
|
42
|
+
|
|
43
|
+
@update_after_operation
|
|
44
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
45
|
+
"""Set brightness."""
|
|
46
|
+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
|
|
47
|
+
hex_brightness = f"{brightness:02X}"
|
|
48
|
+
color_temp = self._state.get("cw", DEFAULT_COLOR_TEMP)
|
|
49
|
+
hex_data = f"{hex_brightness}{color_temp:04X}"
|
|
50
|
+
result = await self._send_command(self._set_brightness_command.format(hex_data))
|
|
51
|
+
return self._check_command_result(result, 0, {1})
|
|
52
|
+
|
|
53
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
54
|
+
"""Get device basic settings."""
|
|
55
|
+
if not (
|
|
56
|
+
res := await self._get_multi_commands_results(self._get_basic_info_command)
|
|
57
|
+
):
|
|
58
|
+
return None
|
|
59
|
+
_version_info, _data = res
|
|
60
|
+
|
|
61
|
+
self._state["cw"] = int.from_bytes(_data[3:5], "big")
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"isOn": bool(_data[1] & 0b10000000),
|
|
65
|
+
"color_mode": _data[1] & 0b01000000,
|
|
66
|
+
"brightness": _data[2] & 0b01111111,
|
|
67
|
+
"cw": self._state["cw"],
|
|
68
|
+
"firmware": _version_info[2] / 10.0,
|
|
69
|
+
}
|