PySwitchbot 0.66.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.66.0 → pyswitchbot-0.67.0}/PKG-INFO +1 -1
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/setup.py +1 -1
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parser.py +7 -2
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/lock.py +17 -4
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/air_purifier.py +3 -14
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/base_light.py +72 -16
- pyswitchbot-0.67.0/switchbot/devices/bulb.py +66 -0
- pyswitchbot-0.67.0/switchbot/devices/ceiling_light.py +69 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/device.py +25 -1
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/evaporative_humidifier.py +3 -13
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/fan.py +2 -14
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/light_strip.py +41 -121
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/lock.py +30 -12
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/relay_switch.py +5 -15
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/vacuum.py +0 -3
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_adv_parser.py +2 -8
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_bulb.py +46 -15
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_ceiling_light.py +16 -10
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_colormode_imports.py +8 -8
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_fan.py +8 -5
- pyswitchbot-0.67.0/tests/test_lock.py +677 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_relay_switch.py +2 -2
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_strip_light.py +74 -17
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_vacuum.py +4 -2
- pyswitchbot-0.66.0/switchbot/devices/bulb.py +0 -143
- pyswitchbot-0.66.0/switchbot/devices/ceiling_light.py +0 -105
- pyswitchbot-0.66.0/tests/test_lock.py +0 -42
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/LICENSE +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/SOURCES.txt +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/README.md +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/pyproject.toml +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/setup.cfg +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/__init__.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/const/__init__.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/const/light.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/enum.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_air_purifier.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_encrypted_device.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_helpers.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_hub3.py +0 -0
- {pyswitchbot-0.66.0 → pyswitchbot-0.67.0}/tests/test_roller_shade.py +0 -0
|
@@ -25,7 +25,12 @@ from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohu
|
|
|
25
25
|
from .adv_parsers.keypad import process_wokeypad
|
|
26
26
|
from .adv_parsers.leak import process_leak
|
|
27
27
|
from .adv_parsers.light_strip import process_light, process_wostrip
|
|
28
|
-
from .adv_parsers.lock import
|
|
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": {
|
|
@@ -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),
|
|
@@ -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")
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from ..helpers import create_background_task
|
|
8
8
|
from ..models import SwitchBotAdvertisement
|
|
9
|
-
from .device import SwitchbotDevice
|
|
9
|
+
from .device import SwitchbotDevice, SwitchbotOperationError, update_after_operation
|
|
10
10
|
|
|
11
11
|
_LOGGER = logging.getLogger(__name__)
|
|
12
12
|
|
|
@@ -14,14 +14,19 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
14
14
|
class SwitchbotBaseLight(SwitchbotDevice):
|
|
15
15
|
"""Representation of a Switchbot light."""
|
|
16
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
|
+
|
|
17
22
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
18
|
-
"""Switchbot
|
|
23
|
+
"""Switchbot base light constructor."""
|
|
19
24
|
super().__init__(*args, **kwargs)
|
|
20
25
|
self._state: dict[str, Any] = {}
|
|
21
26
|
|
|
22
27
|
@property
|
|
23
28
|
def on(self) -> bool | None:
|
|
24
|
-
"""Return if
|
|
29
|
+
"""Return if light is on."""
|
|
25
30
|
return self.is_on()
|
|
26
31
|
|
|
27
32
|
@property
|
|
@@ -60,7 +65,7 @@ class SwitchbotBaseLight(SwitchbotDevice):
|
|
|
60
65
|
@property
|
|
61
66
|
def get_effect_list(self) -> list[str] | None:
|
|
62
67
|
"""Return the list of supported effects."""
|
|
63
|
-
return None
|
|
68
|
+
return list(self._effect_dict) if self._effect_dict else None
|
|
64
69
|
|
|
65
70
|
def is_on(self) -> bool | None:
|
|
66
71
|
"""Return bulb state from cache."""
|
|
@@ -70,25 +75,49 @@ class SwitchbotBaseLight(SwitchbotDevice):
|
|
|
70
75
|
"""Return the current effect."""
|
|
71
76
|
return self._get_adv_value("effect")
|
|
72
77
|
|
|
73
|
-
@
|
|
74
|
-
async def turn_on(self) -> bool:
|
|
75
|
-
"""Turn device on."""
|
|
76
|
-
|
|
77
|
-
@abstractmethod
|
|
78
|
-
async def turn_off(self) -> bool:
|
|
79
|
-
"""Turn device off."""
|
|
80
|
-
|
|
81
|
-
@abstractmethod
|
|
78
|
+
@update_after_operation
|
|
82
79
|
async def set_brightness(self, brightness: int) -> bool:
|
|
83
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})
|
|
84
88
|
|
|
85
|
-
@
|
|
89
|
+
@update_after_operation
|
|
86
90
|
async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
|
|
87
91
|
"""Set color temp."""
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
90
100
|
async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
|
|
91
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
|
|
92
121
|
|
|
93
122
|
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
94
123
|
"""
|
|
@@ -103,6 +132,33 @@ class SwitchbotBaseLight(SwitchbotDevice):
|
|
|
103
132
|
final_result |= self._check_command_result(result, 0, {1})
|
|
104
133
|
return final_result
|
|
105
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
|
+
|
|
106
162
|
|
|
107
163
|
class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
|
|
108
164
|
"""Representation of a Switchbot light."""
|
|
@@ -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
|
+
}
|
|
@@ -125,6 +125,9 @@ def _handle_timeout(fut: asyncio.Future[None]) -> None:
|
|
|
125
125
|
class SwitchbotBaseDevice:
|
|
126
126
|
"""Base Representation of a Switchbot Device."""
|
|
127
127
|
|
|
128
|
+
_turn_on_command: str | None = None
|
|
129
|
+
_turn_off_command: str | None = None
|
|
130
|
+
|
|
128
131
|
def __init__(
|
|
129
132
|
self,
|
|
130
133
|
device: BLEDevice,
|
|
@@ -694,6 +697,27 @@ class SwitchbotBaseDevice:
|
|
|
694
697
|
time_since_last_full_update = time.monotonic() - self._last_full_update
|
|
695
698
|
return not time_since_last_full_update < PASSIVE_POLL_INTERVAL
|
|
696
699
|
|
|
700
|
+
def _check_function_support(self, cmd: str | None = None) -> None:
|
|
701
|
+
"""Check if the command is supported by the device model."""
|
|
702
|
+
if not cmd:
|
|
703
|
+
raise SwitchbotOperationError(
|
|
704
|
+
f"Current device {self._device.address} does not support this functionality"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
@update_after_operation
|
|
708
|
+
async def turn_on(self) -> bool:
|
|
709
|
+
"""Turn device on."""
|
|
710
|
+
self._check_function_support(self._turn_on_command)
|
|
711
|
+
result = await self._send_command(self._turn_on_command)
|
|
712
|
+
return self._check_command_result(result, 0, {1})
|
|
713
|
+
|
|
714
|
+
@update_after_operation
|
|
715
|
+
async def turn_off(self) -> bool:
|
|
716
|
+
"""Turn device off."""
|
|
717
|
+
self._check_function_support(self._turn_off_command)
|
|
718
|
+
result = await self._send_command(self._turn_off_command)
|
|
719
|
+
return self._check_command_result(result, 0, {1})
|
|
720
|
+
|
|
697
721
|
|
|
698
722
|
class SwitchbotDevice(SwitchbotBaseDevice):
|
|
699
723
|
"""
|
|
@@ -735,8 +759,8 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
|
|
|
735
759
|
self._encryption_key = bytearray.fromhex(encryption_key)
|
|
736
760
|
self._iv: bytes | None = None
|
|
737
761
|
self._cipher: bytes | None = None
|
|
738
|
-
self._model = model
|
|
739
762
|
super().__init__(device, None, interface, **kwargs)
|
|
763
|
+
self._model = model
|
|
740
764
|
|
|
741
765
|
# Old non-async method preserved for backwards compatibility
|
|
742
766
|
@classmethod
|
|
@@ -23,7 +23,6 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
23
23
|
COMMAND_HEADER = "57"
|
|
24
24
|
COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
|
|
25
25
|
COMMAND_TURN_ON = f"{COMMAND_HEADER}0f430101"
|
|
26
|
-
COMMAND_TURN_OFF = f"{COMMAND_HEADER}0f430100"
|
|
27
26
|
COMMAND_CHILD_LOCK_ON = f"{COMMAND_HEADER}0f430501"
|
|
28
27
|
COMMAND_CHILD_LOCK_OFF = f"{COMMAND_HEADER}0f430500"
|
|
29
28
|
COMMAND_AUTO_DRY_ON = f"{COMMAND_HEADER}0f430a01"
|
|
@@ -48,6 +47,9 @@ DEVICE_GET_BASIC_SETTINGS_KEY = "570f4481"
|
|
|
48
47
|
class SwitchbotEvaporativeHumidifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
49
48
|
"""Representation of a Switchbot Evaporative Humidifier"""
|
|
50
49
|
|
|
50
|
+
_turn_on_command = COMMAND_TURN_ON
|
|
51
|
+
_turn_off_command = f"{COMMAND_HEADER}0f430100"
|
|
52
|
+
|
|
51
53
|
def __init__(
|
|
52
54
|
self,
|
|
53
55
|
device: BLEDevice,
|
|
@@ -113,18 +115,6 @@ class SwitchbotEvaporativeHumidifier(SwitchbotSequenceDevice, SwitchbotEncrypted
|
|
|
113
115
|
"target_humidity": target_humidity,
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
@update_after_operation
|
|
117
|
-
async def turn_on(self) -> bool:
|
|
118
|
-
"""Turn device on."""
|
|
119
|
-
result = await self._send_command(COMMAND_TURN_ON)
|
|
120
|
-
return self._check_command_result(result, 0, {1})
|
|
121
|
-
|
|
122
|
-
@update_after_operation
|
|
123
|
-
async def turn_off(self) -> bool:
|
|
124
|
-
"""Turn device off."""
|
|
125
|
-
result = await self._send_command(COMMAND_TURN_OFF)
|
|
126
|
-
return self._check_command_result(result, 0, {1})
|
|
127
|
-
|
|
128
118
|
@update_after_operation
|
|
129
119
|
async def set_target_humidity(self, target_humidity: int) -> bool:
|
|
130
120
|
"""Set target humidity."""
|
|
@@ -16,8 +16,6 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
COMMAND_HEAD = "570f41"
|
|
19
|
-
COMMAND_TURN_ON = f"{COMMAND_HEAD}0101"
|
|
20
|
-
COMMAND_TURN_OFF = f"{COMMAND_HEAD}0102"
|
|
21
19
|
COMMAND_START_OSCILLATION = f"{COMMAND_HEAD}020101ff"
|
|
22
20
|
COMMAND_STOP_OSCILLATION = f"{COMMAND_HEAD}020102ff"
|
|
23
21
|
COMMAND_SET_MODE = {
|
|
@@ -33,8 +31,8 @@ COMMAND_GET_BASIC_INFO = "570f428102"
|
|
|
33
31
|
class SwitchbotFan(SwitchbotSequenceDevice):
|
|
34
32
|
"""Representation of a Switchbot Circulator Fan."""
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
_turn_on_command = f"{COMMAND_HEAD}0101"
|
|
35
|
+
_turn_off_command = f"{COMMAND_HEAD}0102"
|
|
38
36
|
|
|
39
37
|
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
40
38
|
"""Get device basic settings."""
|
|
@@ -88,16 +86,6 @@ class SwitchbotFan(SwitchbotSequenceDevice):
|
|
|
88
86
|
return await self._send_command(COMMAND_START_OSCILLATION)
|
|
89
87
|
return await self._send_command(COMMAND_STOP_OSCILLATION)
|
|
90
88
|
|
|
91
|
-
@update_after_operation
|
|
92
|
-
async def turn_on(self) -> bool:
|
|
93
|
-
"""Turn on the fan."""
|
|
94
|
-
return await self._send_command(COMMAND_TURN_ON)
|
|
95
|
-
|
|
96
|
-
@update_after_operation
|
|
97
|
-
async def turn_off(self) -> bool:
|
|
98
|
-
"""Turn off the fan."""
|
|
99
|
-
return await self._send_command(COMMAND_TURN_OFF)
|
|
100
|
-
|
|
101
89
|
def get_current_percentage(self) -> Any:
|
|
102
90
|
"""Return cached percentage."""
|
|
103
91
|
return self._get_adv_value("speed")
|