PySwitchbot 0.65.0__tar.gz → 0.66.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.66.0}/PKG-INFO +1 -1
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/SOURCES.txt +6 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/setup.py +1 -1
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/__init__.py +10 -2
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parser.py +13 -1
- pyswitchbot-0.66.0/switchbot/adv_parsers/light_strip.py +32 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/__init__.py +12 -0
- pyswitchbot-0.66.0/switchbot/const/light.py +34 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/base_light.py +25 -11
- pyswitchbot-0.66.0/switchbot/devices/bulb.py +143 -0
- pyswitchbot-0.66.0/switchbot/devices/ceiling_light.py +105 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/device.py +108 -68
- pyswitchbot-0.66.0/switchbot/devices/light_strip.py +259 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_adv_parser.py +182 -0
- pyswitchbot-0.66.0/tests/test_bulb.py +220 -0
- pyswitchbot-0.66.0/tests/test_ceiling_light.py +179 -0
- pyswitchbot-0.66.0/tests/test_colormode_imports.py +88 -0
- pyswitchbot-0.66.0/tests/test_encrypted_device.py +367 -0
- pyswitchbot-0.66.0/tests/test_strip_light.py +302 -0
- pyswitchbot-0.65.0/switchbot/adv_parsers/light_strip.py +0 -21
- 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 → pyswitchbot-0.66.0}/LICENSE +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/README.md +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/pyproject.toml +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/setup.cfg +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/fan.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/relay_switch.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/enum.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_air_purifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_fan.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_helpers.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_hub3.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_lock.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_relay_switch.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_roller_shade.py +0 -0
- {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_vacuum.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,7 +24,7 @@ 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
|
|
27
|
+
from .adv_parsers.light_strip import process_light, process_wostrip
|
|
28
28
|
from .adv_parsers.lock import process_lock2, process_wolock, process_wolock_pro
|
|
29
29
|
from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
|
|
30
30
|
from .adv_parsers.motion import process_wopresence
|
|
@@ -325,6 +325,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
325
325
|
"func": process_relay_switch_2pm,
|
|
326
326
|
"manufacturer_id": 2409,
|
|
327
327
|
},
|
|
328
|
+
b"\x00\x10\xd0\xb0": {
|
|
329
|
+
"modelName": SwitchbotModel.FLOOR_LAMP,
|
|
330
|
+
"modelFriendlyName": "Floor Lamp",
|
|
331
|
+
"func": process_light,
|
|
332
|
+
"manufacturer_id": 2409,
|
|
333
|
+
},
|
|
334
|
+
b"\x00\x10\xd0\xb1": {
|
|
335
|
+
"modelName": SwitchbotModel.STRIP_LIGHT_3,
|
|
336
|
+
"modelFriendlyName": "Strip Light 3",
|
|
337
|
+
"func": process_light,
|
|
338
|
+
"manufacturer_id": 2409,
|
|
339
|
+
},
|
|
328
340
|
}
|
|
329
341
|
|
|
330
342
|
_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
|
|
@@ -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
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import time
|
|
5
4
|
from abc import abstractmethod
|
|
6
5
|
from typing import Any
|
|
7
6
|
|
|
8
7
|
from ..helpers import create_background_task
|
|
9
8
|
from ..models import SwitchBotAdvertisement
|
|
10
|
-
from .device import
|
|
9
|
+
from .device import SwitchbotDevice
|
|
11
10
|
|
|
12
11
|
_LOGGER = logging.getLogger(__name__)
|
|
13
12
|
|
|
@@ -43,9 +42,10 @@ class SwitchbotBaseLight(SwitchbotDevice):
|
|
|
43
42
|
return self._get_adv_value("brightness") or 0
|
|
44
43
|
|
|
45
44
|
@property
|
|
46
|
-
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def color_mode(self) -> Any:
|
|
47
47
|
"""Return the current color mode."""
|
|
48
|
-
|
|
48
|
+
raise NotImplementedError("Subclasses must implement color mode")
|
|
49
49
|
|
|
50
50
|
@property
|
|
51
51
|
def min_temp(self) -> int:
|
|
@@ -57,10 +57,19 @@ class SwitchbotBaseLight(SwitchbotDevice):
|
|
|
57
57
|
"""Return maximum color temp."""
|
|
58
58
|
return 6500
|
|
59
59
|
|
|
60
|
+
@property
|
|
61
|
+
def get_effect_list(self) -> list[str] | None:
|
|
62
|
+
"""Return the list of supported effects."""
|
|
63
|
+
return None
|
|
64
|
+
|
|
60
65
|
def is_on(self) -> bool | None:
|
|
61
66
|
"""Return bulb state from cache."""
|
|
62
67
|
return self._get_adv_value("isOn")
|
|
63
68
|
|
|
69
|
+
def get_effect(self):
|
|
70
|
+
"""Return the current effect."""
|
|
71
|
+
return self._get_adv_value("effect")
|
|
72
|
+
|
|
64
73
|
@abstractmethod
|
|
65
74
|
async def turn_on(self) -> bool:
|
|
66
75
|
"""Turn device on."""
|
|
@@ -81,13 +90,18 @@ class SwitchbotBaseLight(SwitchbotDevice):
|
|
|
81
90
|
async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
|
|
82
91
|
"""Set rgb."""
|
|
83
92
|
|
|
84
|
-
def
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Send multiple commands to device.
|
|
96
|
+
|
|
97
|
+
Since we current have no way to tell which command the device
|
|
98
|
+
needs we send both.
|
|
99
|
+
"""
|
|
100
|
+
final_result = False
|
|
101
|
+
for key in keys:
|
|
102
|
+
result = await self._send_command(key)
|
|
103
|
+
final_result |= self._check_command_result(result, 0, {1})
|
|
104
|
+
return final_result
|
|
91
105
|
|
|
92
106
|
|
|
93
107
|
class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..const.light import BulbColorMode, ColorMode
|
|
7
|
+
from .base_light import SwitchbotSequenceBaseLight
|
|
8
|
+
from .device import REQ_HEADER, SwitchbotOperationError, update_after_operation
|
|
9
|
+
|
|
10
|
+
BULB_COMMAND_HEADER = "4701"
|
|
11
|
+
BULB_REQUEST = f"{REQ_HEADER}4801"
|
|
12
|
+
|
|
13
|
+
BULB_COMMAND = f"{REQ_HEADER}{BULB_COMMAND_HEADER}"
|
|
14
|
+
# Bulb keys
|
|
15
|
+
BULB_ON_KEY = f"{BULB_COMMAND}01"
|
|
16
|
+
BULB_OFF_KEY = f"{BULB_COMMAND}02"
|
|
17
|
+
RGB_BRIGHTNESS_KEY = f"{BULB_COMMAND}12"
|
|
18
|
+
CW_BRIGHTNESS_KEY = f"{BULB_COMMAND}13"
|
|
19
|
+
BRIGHTNESS_KEY = f"{BULB_COMMAND}14"
|
|
20
|
+
RGB_KEY = f"{BULB_COMMAND}16"
|
|
21
|
+
CW_KEY = f"{BULB_COMMAND}17"
|
|
22
|
+
|
|
23
|
+
DEVICE_GET_VERSION_KEY = "570003"
|
|
24
|
+
DEVICE_GET_BASIC_SETTINGS_KEY = "570f4801"
|
|
25
|
+
|
|
26
|
+
_LOGGER = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
EFFECT_DICT = {
|
|
30
|
+
"Colorful": "570F4701010300",
|
|
31
|
+
"Flickering": "570F4701010301",
|
|
32
|
+
"Breathing": "570F4701010302",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Private mapping from device-specific color modes to original ColorMode enum
|
|
36
|
+
_BULB_COLOR_MODE_MAP = {
|
|
37
|
+
BulbColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
|
38
|
+
BulbColorMode.RGB: ColorMode.RGB,
|
|
39
|
+
BulbColorMode.DYNAMIC: ColorMode.EFFECT,
|
|
40
|
+
BulbColorMode.UNKNOWN: ColorMode.OFF,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SwitchbotBulb(SwitchbotSequenceBaseLight):
|
|
45
|
+
"""Representation of a Switchbot bulb."""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def color_modes(self) -> set[ColorMode]:
|
|
49
|
+
"""Return the supported color modes."""
|
|
50
|
+
return {ColorMode.RGB, ColorMode.COLOR_TEMP}
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def color_mode(self) -> ColorMode:
|
|
54
|
+
"""Return the current color mode."""
|
|
55
|
+
device_mode = BulbColorMode(self._get_adv_value("color_mode") or 10)
|
|
56
|
+
return _BULB_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def get_effect_list(self) -> list[str]:
|
|
60
|
+
"""Return the list of supported effects."""
|
|
61
|
+
return list(EFFECT_DICT.keys())
|
|
62
|
+
|
|
63
|
+
@update_after_operation
|
|
64
|
+
async def turn_on(self) -> bool:
|
|
65
|
+
"""Turn device on."""
|
|
66
|
+
result = await self._send_command(BULB_ON_KEY)
|
|
67
|
+
return self._check_command_result(result, 0, {1})
|
|
68
|
+
|
|
69
|
+
@update_after_operation
|
|
70
|
+
async def turn_off(self) -> bool:
|
|
71
|
+
"""Turn device off."""
|
|
72
|
+
result = await self._send_command(BULB_OFF_KEY)
|
|
73
|
+
return self._check_command_result(result, 0, {1})
|
|
74
|
+
|
|
75
|
+
@update_after_operation
|
|
76
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
77
|
+
"""Set brightness."""
|
|
78
|
+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
|
|
79
|
+
result = await self._send_command(f"{BRIGHTNESS_KEY}{brightness:02X}")
|
|
80
|
+
return self._check_command_result(result, 0, {1})
|
|
81
|
+
|
|
82
|
+
@update_after_operation
|
|
83
|
+
async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
|
|
84
|
+
"""Set color temp."""
|
|
85
|
+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
|
|
86
|
+
assert 2700 <= color_temp <= 6500, "Color Temp must be between 2700 and 6500"
|
|
87
|
+
result = await self._send_command(
|
|
88
|
+
f"{CW_BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
|
|
89
|
+
)
|
|
90
|
+
return self._check_command_result(result, 0, {1})
|
|
91
|
+
|
|
92
|
+
@update_after_operation
|
|
93
|
+
async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
|
|
94
|
+
"""Set rgb."""
|
|
95
|
+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
|
|
96
|
+
assert 0 <= r <= 255, "r must be between 0 and 255"
|
|
97
|
+
assert 0 <= g <= 255, "g must be between 0 and 255"
|
|
98
|
+
assert 0 <= b <= 255, "b must be between 0 and 255"
|
|
99
|
+
result = await self._send_command(
|
|
100
|
+
f"{RGB_BRIGHTNESS_KEY}{brightness:02X}{r:02X}{g:02X}{b:02X}"
|
|
101
|
+
)
|
|
102
|
+
return self._check_command_result(result, 0, {1})
|
|
103
|
+
|
|
104
|
+
@update_after_operation
|
|
105
|
+
async def set_effect(self, effect: str) -> bool:
|
|
106
|
+
"""Set effect."""
|
|
107
|
+
effect_template = EFFECT_DICT.get(effect)
|
|
108
|
+
if not effect_template:
|
|
109
|
+
raise SwitchbotOperationError(f"Effect {effect} not supported")
|
|
110
|
+
result = await self._send_command(effect_template)
|
|
111
|
+
if result:
|
|
112
|
+
self._override_state({"effect": effect})
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
116
|
+
"""Get device basic settings."""
|
|
117
|
+
if not (_data := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
|
|
118
|
+
return None
|
|
119
|
+
if not (_version_info := await self._get_basic_info(DEVICE_GET_VERSION_KEY)):
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
_LOGGER.debug(
|
|
123
|
+
"data: %s, version info: %s, address: %s",
|
|
124
|
+
_data,
|
|
125
|
+
_version_info,
|
|
126
|
+
self._device.address,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self._state["r"] = _data[3]
|
|
130
|
+
self._state["g"] = _data[4]
|
|
131
|
+
self._state["b"] = _data[5]
|
|
132
|
+
self._state["cw"] = int.from_bytes(_data[6:8], "big")
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"isOn": bool(_data[1] & 0b10000000),
|
|
136
|
+
"brightness": _data[2] & 0b01111111,
|
|
137
|
+
"r": self._state["r"],
|
|
138
|
+
"g": self._state["g"],
|
|
139
|
+
"b": self._state["b"],
|
|
140
|
+
"cw": self._state["cw"],
|
|
141
|
+
"color_mode": _data[10] & 0b00001111,
|
|
142
|
+
"firmware": _version_info[2] / 10.0,
|
|
143
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..const.light import (
|
|
7
|
+
DEFAULT_COLOR_TEMP,
|
|
8
|
+
CeilingLightColorMode,
|
|
9
|
+
ColorMode,
|
|
10
|
+
)
|
|
11
|
+
from .base_light import SwitchbotSequenceBaseLight
|
|
12
|
+
from .device import REQ_HEADER, update_after_operation
|
|
13
|
+
|
|
14
|
+
CEILING_LIGHT_COMMAND_HEADER = "5401"
|
|
15
|
+
CEILING_LIGHT_REQUEST = f"{REQ_HEADER}5501"
|
|
16
|
+
|
|
17
|
+
CEILING_LIGHT_COMMAND = f"{REQ_HEADER}{CEILING_LIGHT_COMMAND_HEADER}"
|
|
18
|
+
CEILING_LIGHT_ON_KEY = f"{CEILING_LIGHT_COMMAND}01FF01FFFF"
|
|
19
|
+
CEILING_LIGHT_OFF_KEY = f"{CEILING_LIGHT_COMMAND}02FF01FFFF"
|
|
20
|
+
CW_BRIGHTNESS_KEY = f"{CEILING_LIGHT_COMMAND}010001"
|
|
21
|
+
BRIGHTNESS_KEY = f"{CEILING_LIGHT_COMMAND}01FF01"
|
|
22
|
+
|
|
23
|
+
DEVICE_GET_VERSION_KEY = "5702"
|
|
24
|
+
DEVICE_GET_BASIC_SETTINGS_KEY = "570f5581"
|
|
25
|
+
|
|
26
|
+
_LOGGER = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Private mapping from device-specific color modes to original ColorMode enum
|
|
29
|
+
_CEILING_LIGHT_COLOR_MODE_MAP = {
|
|
30
|
+
CeilingLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
|
31
|
+
CeilingLightColorMode.NIGHT: ColorMode.COLOR_TEMP,
|
|
32
|
+
CeilingLightColorMode.MUSIC: ColorMode.EFFECT,
|
|
33
|
+
CeilingLightColorMode.UNKNOWN: ColorMode.OFF,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SwitchbotCeilingLight(SwitchbotSequenceBaseLight):
|
|
38
|
+
"""Representation of a Switchbot ceiling light."""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def color_modes(self) -> set[ColorMode]:
|
|
42
|
+
"""Return the supported color modes."""
|
|
43
|
+
return {ColorMode.COLOR_TEMP}
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def color_mode(self) -> ColorMode:
|
|
47
|
+
"""Return the current color mode."""
|
|
48
|
+
device_mode = CeilingLightColorMode(self._get_adv_value("color_mode") or 10)
|
|
49
|
+
return _CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
|
|
50
|
+
|
|
51
|
+
@update_after_operation
|
|
52
|
+
async def turn_on(self) -> bool:
|
|
53
|
+
"""Turn device on."""
|
|
54
|
+
result = await self._send_command(CEILING_LIGHT_ON_KEY)
|
|
55
|
+
return self._check_command_result(result, 0, {1})
|
|
56
|
+
|
|
57
|
+
@update_after_operation
|
|
58
|
+
async def turn_off(self) -> bool:
|
|
59
|
+
"""Turn device off."""
|
|
60
|
+
result = await self._send_command(CEILING_LIGHT_OFF_KEY)
|
|
61
|
+
return self._check_command_result(result, 0, {1})
|
|
62
|
+
|
|
63
|
+
@update_after_operation
|
|
64
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
65
|
+
"""Set brightness."""
|
|
66
|
+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
|
|
67
|
+
color_temp = self._state.get("cw", DEFAULT_COLOR_TEMP)
|
|
68
|
+
result = await self._send_command(
|
|
69
|
+
f"{BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
|
|
70
|
+
)
|
|
71
|
+
return self._check_command_result(result, 0, {1})
|
|
72
|
+
|
|
73
|
+
@update_after_operation
|
|
74
|
+
async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
|
|
75
|
+
"""Set color temp."""
|
|
76
|
+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
|
|
77
|
+
assert 2700 <= color_temp <= 6500, "Color Temp must be between 2700 and 6500"
|
|
78
|
+
result = await self._send_command(
|
|
79
|
+
f"{CW_BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
|
|
80
|
+
)
|
|
81
|
+
return self._check_command_result(result, 0, {1})
|
|
82
|
+
|
|
83
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
84
|
+
"""Get device basic settings."""
|
|
85
|
+
if not (_data := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
|
|
86
|
+
return None
|
|
87
|
+
if not (_version_info := await self._get_basic_info(DEVICE_GET_VERSION_KEY)):
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
_LOGGER.debug(
|
|
91
|
+
"data: %s, version info: %s, address: %s",
|
|
92
|
+
_data,
|
|
93
|
+
_version_info,
|
|
94
|
+
self._device.address,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self._state["cw"] = int.from_bytes(_data[3:5], "big")
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"isOn": bool(_data[1] & 0b10000000),
|
|
101
|
+
"color_mode": _data[1] & 0b01000000,
|
|
102
|
+
"brightness": _data[2] & 0b01111111,
|
|
103
|
+
"cw": self._state["cw"],
|
|
104
|
+
"firmware": _version_info[2] / 10.0,
|
|
105
|
+
}
|