PySwitchbot 0.68.4__tar.gz → 0.70.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.68.4 → pyswitchbot-0.70.0}/PKG-INFO +1 -1
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/SOURCES.txt +3 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/setup.py +1 -1
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/__init__.py +8 -1
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parser.py +31 -1
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/light_strip.py +12 -3
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/__init__.py +3 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/light.py +11 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/ceiling_light.py +3 -1
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/device.py +155 -24
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/light_strip.py +167 -1
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/relay_switch.py +1 -0
- pyswitchbot-0.70.0/switchbot/utils.py +24 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_adv_parser.py +194 -1
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_ceiling_light.py +20 -1
- pyswitchbot-0.70.0/tests/test_device.py +379 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_relay_switch.py +1 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_strip_light.py +104 -57
- pyswitchbot-0.70.0/tests/test_utils.py +41 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/LICENSE +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/README.md +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/pyproject.toml +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/setup.cfg +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/fan.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/enum.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_air_purifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_bulb.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_colormode_imports.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_encrypted_device.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_fan.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_helpers.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_hub3.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_lock.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_roller_shade.py +0 -0
- {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_vacuum.py +0 -0
|
@@ -15,6 +15,7 @@ switchbot/discovery.py
|
|
|
15
15
|
switchbot/enum.py
|
|
16
16
|
switchbot/helpers.py
|
|
17
17
|
switchbot/models.py
|
|
18
|
+
switchbot/utils.py
|
|
18
19
|
switchbot/adv_parsers/__init__.py
|
|
19
20
|
switchbot/adv_parsers/air_purifier.py
|
|
20
21
|
switchbot/adv_parsers/blind_tilt.py
|
|
@@ -78,6 +79,7 @@ tests/test_bulb.py
|
|
|
78
79
|
tests/test_ceiling_light.py
|
|
79
80
|
tests/test_colormode_imports.py
|
|
80
81
|
tests/test_curtain.py
|
|
82
|
+
tests/test_device.py
|
|
81
83
|
tests/test_encrypted_device.py
|
|
82
84
|
tests/test_evaporative_humidifier.py
|
|
83
85
|
tests/test_fan.py
|
|
@@ -88,4 +90,5 @@ tests/test_lock.py
|
|
|
88
90
|
tests/test_relay_switch.py
|
|
89
91
|
tests/test_roller_shade.py
|
|
90
92
|
tests/test_strip_light.py
|
|
93
|
+
tests/test_utils.py
|
|
91
94
|
tests/test_vacuum.py
|
|
@@ -36,11 +36,16 @@ from .devices.device import (
|
|
|
36
36
|
SwitchbotDevice,
|
|
37
37
|
SwitchbotEncryptedDevice,
|
|
38
38
|
SwitchbotOperationError,
|
|
39
|
+
fetch_cloud_devices,
|
|
39
40
|
)
|
|
40
41
|
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
|
|
41
42
|
from .devices.fan import SwitchbotFan
|
|
42
43
|
from .devices.humidifier import SwitchbotHumidifier
|
|
43
|
-
from .devices.light_strip import
|
|
44
|
+
from .devices.light_strip import (
|
|
45
|
+
SwitchbotLightStrip,
|
|
46
|
+
SwitchbotRgbicLight,
|
|
47
|
+
SwitchbotStripLight3,
|
|
48
|
+
)
|
|
44
49
|
from .devices.lock import SwitchbotLock
|
|
45
50
|
from .devices.plug import SwitchbotPlugMini
|
|
46
51
|
from .devices.relay_switch import (
|
|
@@ -92,6 +97,7 @@ __all__ = [
|
|
|
92
97
|
"SwitchbotPlugMini",
|
|
93
98
|
"SwitchbotRelaySwitch",
|
|
94
99
|
"SwitchbotRelaySwitch2PM",
|
|
100
|
+
"SwitchbotRgbicLight",
|
|
95
101
|
"SwitchbotRollerShade",
|
|
96
102
|
"SwitchbotStripLight3",
|
|
97
103
|
"SwitchbotSupportedType",
|
|
@@ -99,6 +105,7 @@ __all__ = [
|
|
|
99
105
|
"SwitchbotVacuum",
|
|
100
106
|
"close_stale_connections",
|
|
101
107
|
"close_stale_connections_by_address",
|
|
108
|
+
"fetch_cloud_devices",
|
|
102
109
|
"get_device",
|
|
103
110
|
"parse_advertisement_data",
|
|
104
111
|
]
|
|
@@ -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_light, process_wostrip
|
|
27
|
+
from .adv_parsers.light_strip import process_light, process_rgbic_light, process_wostrip
|
|
28
28
|
from .adv_parsers.lock import (
|
|
29
29
|
process_lock2,
|
|
30
30
|
process_locklite,
|
|
@@ -45,6 +45,7 @@ from .adv_parsers.roller_shade import process_worollershade
|
|
|
45
45
|
from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
|
|
46
46
|
from .const import SwitchbotModel
|
|
47
47
|
from .models import SwitchBotAdvertisement
|
|
48
|
+
from .utils import format_mac_upper
|
|
48
49
|
|
|
49
50
|
_LOGGER = logging.getLogger(__name__)
|
|
50
51
|
|
|
@@ -54,6 +55,8 @@ SERVICE_DATA_ORDER = (
|
|
|
54
55
|
)
|
|
55
56
|
MFR_DATA_ORDER = (2409, 741, 89)
|
|
56
57
|
|
|
58
|
+
_MODEL_TO_MAC_CACHE: dict[str, SwitchbotModel] = {}
|
|
59
|
+
|
|
57
60
|
|
|
58
61
|
class SwitchbotSupportedType(TypedDict):
|
|
59
62
|
"""Supported type of Switchbot."""
|
|
@@ -343,6 +346,24 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
343
346
|
"func": process_light,
|
|
344
347
|
"manufacturer_id": 2409,
|
|
345
348
|
},
|
|
349
|
+
"?": {
|
|
350
|
+
"modelName": SwitchbotModel.PLUG_MINI_EU,
|
|
351
|
+
"modelFriendlyName": "Plug Mini (EU)",
|
|
352
|
+
"func": process_relay_switch_1pm,
|
|
353
|
+
"manufacturer_id": 2409,
|
|
354
|
+
},
|
|
355
|
+
b"\x00\x10\xd0\xb3": {
|
|
356
|
+
"modelName": SwitchbotModel.RGBICWW_STRIP_LIGHT,
|
|
357
|
+
"modelFriendlyName": "RGBICWW Strip Light",
|
|
358
|
+
"func": process_rgbic_light,
|
|
359
|
+
"manufacturer_id": 2409,
|
|
360
|
+
},
|
|
361
|
+
b"\x00\x10\xd0\xb4": {
|
|
362
|
+
"modelName": SwitchbotModel.RGBICWW_FLOOR_LAMP,
|
|
363
|
+
"modelFriendlyName": "RGBICWW Floor Lamp",
|
|
364
|
+
"func": process_rgbic_light,
|
|
365
|
+
"manufacturer_id": 2409,
|
|
366
|
+
},
|
|
346
367
|
}
|
|
347
368
|
|
|
348
369
|
_SWITCHBOT_MODEL_TO_CHAR = {
|
|
@@ -365,6 +386,10 @@ def parse_advertisement_data(
|
|
|
365
386
|
model: SwitchbotModel | None = None,
|
|
366
387
|
) -> SwitchBotAdvertisement | None:
|
|
367
388
|
"""Parse advertisement data."""
|
|
389
|
+
upper_mac = format_mac_upper(device.address)
|
|
390
|
+
if model is None and upper_mac in _MODEL_TO_MAC_CACHE:
|
|
391
|
+
model = _MODEL_TO_MAC_CACHE[upper_mac]
|
|
392
|
+
|
|
368
393
|
service_data = advertisement_data.service_data
|
|
369
394
|
|
|
370
395
|
_service_data = None
|
|
@@ -452,3 +477,8 @@ def _parse_data(
|
|
|
452
477
|
)
|
|
453
478
|
|
|
454
479
|
return data
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def populate_model_to_mac_cache(mac: str, model: SwitchbotModel) -> None:
|
|
483
|
+
"""Populate the model to MAC address cache."""
|
|
484
|
+
_MODEL_TO_MAC_CACHE[mac] = model
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
from ..helpers import _UNPACK_UINT16_BE
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def process_wostrip(
|
|
@@ -21,12 +21,21 @@ def process_wostrip(
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def process_light(
|
|
24
|
+
def process_light(
|
|
25
|
+
data: bytes | None, mfr_data: bytes | None, cw_offset: int = 16
|
|
26
|
+
) -> dict[str, bool | int]:
|
|
25
27
|
"""Support for strip light 3 and floor lamp."""
|
|
26
28
|
common_data = process_wostrip(data, mfr_data)
|
|
27
29
|
if not common_data:
|
|
28
30
|
return {}
|
|
29
31
|
|
|
30
|
-
light_data = {"cw":
|
|
32
|
+
light_data = {"cw": _UNPACK_UINT16_BE(mfr_data, cw_offset)[0]}
|
|
31
33
|
|
|
32
34
|
return common_data | light_data
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def process_rgbic_light(
|
|
38
|
+
data: bytes | None, mfr_data: bytes | None
|
|
39
|
+
) -> dict[str, bool | int]:
|
|
40
|
+
"""Support for RGBIC lights."""
|
|
41
|
+
return process_light(data, mfr_data, cw_offset=10)
|
|
@@ -93,6 +93,9 @@ class SwitchbotModel(StrEnum):
|
|
|
93
93
|
RELAY_SWITCH_2PM = "Relay Switch 2PM"
|
|
94
94
|
STRIP_LIGHT_3 = "Strip Light 3"
|
|
95
95
|
FLOOR_LAMP = "Floor Lamp"
|
|
96
|
+
PLUG_MINI_EU = "Plug Mini (EU)"
|
|
97
|
+
RGBICWW_STRIP_LIGHT = "RGBICWW Strip Light"
|
|
98
|
+
RGBICWW_FLOOR_LAMP = "RGBICWW Floor Lamp"
|
|
96
99
|
|
|
97
100
|
|
|
98
101
|
__all__ = [
|
|
@@ -31,4 +31,15 @@ class CeilingLightColorMode(Enum):
|
|
|
31
31
|
UNKNOWN = 10
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class RGBICStripLightColorMode(Enum):
|
|
35
|
+
SEGMENTED = 1
|
|
36
|
+
RGB = 2
|
|
37
|
+
SCENE = 3
|
|
38
|
+
MUSIC = 4
|
|
39
|
+
CONTROLLER = 5
|
|
40
|
+
COLOR_TEMP = 6
|
|
41
|
+
EFFECT = 7
|
|
42
|
+
UNKNOWN = 10
|
|
43
|
+
|
|
44
|
+
|
|
34
45
|
DEFAULT_COLOR_TEMP = 4001
|
|
@@ -37,7 +37,9 @@ class SwitchbotCeilingLight(SwitchbotSequenceBaseLight):
|
|
|
37
37
|
@property
|
|
38
38
|
def color_mode(self) -> ColorMode:
|
|
39
39
|
"""Return the current color mode."""
|
|
40
|
-
device_mode = CeilingLightColorMode(
|
|
40
|
+
device_mode = CeilingLightColorMode(
|
|
41
|
+
value if (value := self._get_adv_value("color_mode")) is not None else 10
|
|
42
|
+
)
|
|
41
43
|
return _CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
|
|
42
44
|
|
|
43
45
|
@update_after_operation
|
|
@@ -24,6 +24,7 @@ from bleak_retry_connector import (
|
|
|
24
24
|
)
|
|
25
25
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
26
26
|
|
|
27
|
+
from ..adv_parser import populate_model_to_mac_cache
|
|
27
28
|
from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_CLIENT_ID
|
|
28
29
|
from ..const import (
|
|
29
30
|
DEFAULT_RETRY_COUNT,
|
|
@@ -37,9 +38,43 @@ from ..const import (
|
|
|
37
38
|
from ..discovery import GetSwitchbotDevices
|
|
38
39
|
from ..helpers import create_background_task
|
|
39
40
|
from ..models import SwitchBotAdvertisement
|
|
41
|
+
from ..utils import format_mac_upper
|
|
40
42
|
|
|
41
43
|
_LOGGER = logging.getLogger(__name__)
|
|
42
44
|
|
|
45
|
+
|
|
46
|
+
def _extract_region(userinfo: dict[str, Any]) -> str:
|
|
47
|
+
"""Extract region from user info, defaulting to 'us'."""
|
|
48
|
+
if "botRegion" in userinfo and userinfo["botRegion"] != "":
|
|
49
|
+
return userinfo["botRegion"]
|
|
50
|
+
return "us"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Mapping from API model names to SwitchbotModel enum values
|
|
54
|
+
API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
|
|
55
|
+
"WoHand": SwitchbotModel.BOT,
|
|
56
|
+
"WoCurtain": SwitchbotModel.CURTAIN,
|
|
57
|
+
"WoHumi": SwitchbotModel.HUMIDIFIER,
|
|
58
|
+
"WoPlug": SwitchbotModel.PLUG_MINI,
|
|
59
|
+
"WoPlugUS": SwitchbotModel.PLUG_MINI,
|
|
60
|
+
"WoContact": SwitchbotModel.CONTACT_SENSOR,
|
|
61
|
+
"WoStrip": SwitchbotModel.LIGHT_STRIP,
|
|
62
|
+
"WoSensorTH": SwitchbotModel.METER,
|
|
63
|
+
"WoMeter": SwitchbotModel.METER,
|
|
64
|
+
"WoMeterPlus": SwitchbotModel.METER_PRO,
|
|
65
|
+
"WoPresence": SwitchbotModel.MOTION_SENSOR,
|
|
66
|
+
"WoBulb": SwitchbotModel.COLOR_BULB,
|
|
67
|
+
"WoCeiling": SwitchbotModel.CEILING_LIGHT,
|
|
68
|
+
"WoLock": SwitchbotModel.LOCK,
|
|
69
|
+
"WoBlindTilt": SwitchbotModel.BLIND_TILT,
|
|
70
|
+
"WoIOSensor": SwitchbotModel.IO_METER, # Outdoor Meter
|
|
71
|
+
"WoButton": SwitchbotModel.REMOTE, # Remote button
|
|
72
|
+
"WoLinkMini": SwitchbotModel.HUBMINI_MATTER, # Hub Mini
|
|
73
|
+
"W1083002": SwitchbotModel.RELAY_SWITCH_1, # Relay Switch 1
|
|
74
|
+
"W1079000": SwitchbotModel.METER_PRO, # Meter Pro (another variant)
|
|
75
|
+
"W1102001": SwitchbotModel.STRIP_LIGHT_3, # RGBWW Strip Light 3
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
REQ_HEADER = "570f"
|
|
44
79
|
|
|
45
80
|
|
|
@@ -164,6 +199,113 @@ class SwitchbotBaseDevice:
|
|
|
164
199
|
self._last_full_update: float = -PASSIVE_POLL_INTERVAL
|
|
165
200
|
self._timed_disconnect_task: asyncio.Task[None] | None = None
|
|
166
201
|
|
|
202
|
+
@classmethod
|
|
203
|
+
async def _async_get_user_info(
|
|
204
|
+
cls,
|
|
205
|
+
session: aiohttp.ClientSession,
|
|
206
|
+
auth_headers: dict[str, str],
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
try:
|
|
209
|
+
return await cls.api_request(
|
|
210
|
+
session, "account", "account/api/v1/user/userinfo", {}, auth_headers
|
|
211
|
+
)
|
|
212
|
+
except Exception as err:
|
|
213
|
+
raise SwitchbotAccountConnectionError(
|
|
214
|
+
f"Failed to retrieve SwitchBot Account user details: {err}"
|
|
215
|
+
) from err
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
async def _get_auth_result(
|
|
219
|
+
cls,
|
|
220
|
+
session: aiohttp.ClientSession,
|
|
221
|
+
username: str,
|
|
222
|
+
password: str,
|
|
223
|
+
) -> dict[str, Any]:
|
|
224
|
+
"""Authenticate with SwitchBot API."""
|
|
225
|
+
try:
|
|
226
|
+
return await cls.api_request(
|
|
227
|
+
session,
|
|
228
|
+
"account",
|
|
229
|
+
"account/api/v1/user/login",
|
|
230
|
+
{
|
|
231
|
+
"clientId": SWITCHBOT_APP_CLIENT_ID,
|
|
232
|
+
"username": username,
|
|
233
|
+
"password": password,
|
|
234
|
+
"grantType": "password",
|
|
235
|
+
"verifyCode": "",
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
except Exception as err:
|
|
239
|
+
raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
async def get_devices(
|
|
243
|
+
cls,
|
|
244
|
+
session: aiohttp.ClientSession,
|
|
245
|
+
username: str,
|
|
246
|
+
password: str,
|
|
247
|
+
) -> dict[str, SwitchbotModel]:
|
|
248
|
+
"""Get devices from SwitchBot API and return formatted MAC to model mapping."""
|
|
249
|
+
try:
|
|
250
|
+
auth_result = await cls._get_auth_result(session, username, password)
|
|
251
|
+
auth_headers = {"authorization": auth_result["access_token"]}
|
|
252
|
+
except Exception as err:
|
|
253
|
+
raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err
|
|
254
|
+
|
|
255
|
+
userinfo = await cls._async_get_user_info(session, auth_headers)
|
|
256
|
+
region = _extract_region(userinfo)
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
device_info = await cls.api_request(
|
|
260
|
+
session,
|
|
261
|
+
f"wonderlabs.{region}",
|
|
262
|
+
"wonder/device/v3/getdevice",
|
|
263
|
+
{
|
|
264
|
+
"required_type": "All",
|
|
265
|
+
},
|
|
266
|
+
auth_headers,
|
|
267
|
+
)
|
|
268
|
+
except Exception as err:
|
|
269
|
+
raise SwitchbotAccountConnectionError(
|
|
270
|
+
f"Failed to retrieve devices from SwitchBot Account: {err}"
|
|
271
|
+
) from err
|
|
272
|
+
|
|
273
|
+
items: list[dict[str, Any]] = device_info["Items"]
|
|
274
|
+
mac_to_model: dict[str, SwitchbotModel] = {}
|
|
275
|
+
|
|
276
|
+
for item in items:
|
|
277
|
+
if "device_mac" not in item:
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
"device_detail" not in item
|
|
282
|
+
or "device_type" not in item["device_detail"]
|
|
283
|
+
):
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
mac = item["device_mac"]
|
|
287
|
+
model_name = item["device_detail"]["device_type"]
|
|
288
|
+
|
|
289
|
+
# Format MAC to uppercase with colons
|
|
290
|
+
formatted_mac = format_mac_upper(mac)
|
|
291
|
+
|
|
292
|
+
# Map API model name to SwitchbotModel enum if possible
|
|
293
|
+
if model_name in API_MODEL_TO_ENUM:
|
|
294
|
+
model = API_MODEL_TO_ENUM[model_name]
|
|
295
|
+
mac_to_model[formatted_mac] = model
|
|
296
|
+
# Populate the cache
|
|
297
|
+
populate_model_to_mac_cache(formatted_mac, model)
|
|
298
|
+
else:
|
|
299
|
+
# Log the full item payload for unknown models
|
|
300
|
+
_LOGGER.debug(
|
|
301
|
+
"Unknown model %s for device %s, full item: %s",
|
|
302
|
+
model_name,
|
|
303
|
+
formatted_mac,
|
|
304
|
+
item,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return mac_to_model
|
|
308
|
+
|
|
167
309
|
@classmethod
|
|
168
310
|
async def api_request(
|
|
169
311
|
cls,
|
|
@@ -809,34 +951,13 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
|
|
|
809
951
|
device_mac = device_mac.replace(":", "").replace("-", "").upper()
|
|
810
952
|
|
|
811
953
|
try:
|
|
812
|
-
auth_result = await cls.
|
|
813
|
-
session,
|
|
814
|
-
"account",
|
|
815
|
-
"account/api/v1/user/login",
|
|
816
|
-
{
|
|
817
|
-
"clientId": SWITCHBOT_APP_CLIENT_ID,
|
|
818
|
-
"username": username,
|
|
819
|
-
"password": password,
|
|
820
|
-
"grantType": "password",
|
|
821
|
-
"verifyCode": "",
|
|
822
|
-
},
|
|
823
|
-
)
|
|
954
|
+
auth_result = await cls._get_auth_result(session, username, password)
|
|
824
955
|
auth_headers = {"authorization": auth_result["access_token"]}
|
|
825
956
|
except Exception as err:
|
|
826
957
|
raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err
|
|
827
958
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
session, "account", "account/api/v1/user/userinfo", {}, auth_headers
|
|
831
|
-
)
|
|
832
|
-
if "botRegion" in userinfo and userinfo["botRegion"] != "":
|
|
833
|
-
region = userinfo["botRegion"]
|
|
834
|
-
else:
|
|
835
|
-
region = "us"
|
|
836
|
-
except Exception as err:
|
|
837
|
-
raise SwitchbotAccountConnectionError(
|
|
838
|
-
f"Failed to retrieve SwitchBot Account user details: {err}"
|
|
839
|
-
) from err
|
|
959
|
+
userinfo = await cls._async_get_user_info(session, auth_headers)
|
|
960
|
+
region = _extract_region(userinfo)
|
|
840
961
|
|
|
841
962
|
try:
|
|
842
963
|
device_info = await cls.api_request(
|
|
@@ -1023,3 +1144,13 @@ class SwitchbotSequenceDevice(SwitchbotDevice):
|
|
|
1023
1144
|
)
|
|
1024
1145
|
if current_state != new_state:
|
|
1025
1146
|
create_background_task(self.update())
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
async def fetch_cloud_devices(
|
|
1150
|
+
session: aiohttp.ClientSession,
|
|
1151
|
+
username: str,
|
|
1152
|
+
password: str,
|
|
1153
|
+
) -> dict[str, SwitchbotModel]:
|
|
1154
|
+
"""Fetch devices from SwitchBot API and return MAC to model mapping."""
|
|
1155
|
+
# Get devices from the API (which also populates the cache)
|
|
1156
|
+
return await SwitchbotBaseDevice.get_devices(session, username, password)
|
|
@@ -5,7 +5,7 @@ from typing import Any
|
|
|
5
5
|
from bleak.backends.device import BLEDevice
|
|
6
6
|
|
|
7
7
|
from ..const import SwitchbotModel
|
|
8
|
-
from ..const.light import ColorMode, StripLightColorMode
|
|
8
|
+
from ..const.light import ColorMode, RGBICStripLightColorMode, StripLightColorMode
|
|
9
9
|
from .base_light import SwitchbotSequenceBaseLight
|
|
10
10
|
from .device import SwitchbotEncryptedDevice
|
|
11
11
|
|
|
@@ -18,6 +18,15 @@ _STRIP_LIGHT_COLOR_MODE_MAP = {
|
|
|
18
18
|
StripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
|
19
19
|
StripLightColorMode.UNKNOWN: ColorMode.OFF,
|
|
20
20
|
}
|
|
21
|
+
_RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP = {
|
|
22
|
+
RGBICStripLightColorMode.SEGMENTED: ColorMode.EFFECT,
|
|
23
|
+
RGBICStripLightColorMode.RGB: ColorMode.RGB,
|
|
24
|
+
RGBICStripLightColorMode.SCENE: ColorMode.EFFECT,
|
|
25
|
+
RGBICStripLightColorMode.MUSIC: ColorMode.EFFECT,
|
|
26
|
+
RGBICStripLightColorMode.CONTROLLER: ColorMode.EFFECT,
|
|
27
|
+
RGBICStripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
|
28
|
+
RGBICStripLightColorMode.UNKNOWN: ColorMode.OFF,
|
|
29
|
+
}
|
|
21
30
|
LIGHT_STRIP_CONTROL_HEADER = "570F4901"
|
|
22
31
|
COMMON_EFFECTS = {
|
|
23
32
|
"christmas": [
|
|
@@ -97,6 +106,122 @@ COMMON_EFFECTS = {
|
|
|
97
106
|
"570F490701000503600C2B35040C",
|
|
98
107
|
],
|
|
99
108
|
}
|
|
109
|
+
RGBIC_EFFECTS = {
|
|
110
|
+
"romance": [
|
|
111
|
+
"570F490D01350100FF10EE",
|
|
112
|
+
"570F490D0363",
|
|
113
|
+
],
|
|
114
|
+
"energy": [
|
|
115
|
+
"570F490D01000300ED070F34FF14FFE114",
|
|
116
|
+
"570F490D03FA",
|
|
117
|
+
],
|
|
118
|
+
"heartbeat": [
|
|
119
|
+
"570F490D01020400FFDEADFE90FDFF9E3D",
|
|
120
|
+
"570F490D01020403FCBAFD",
|
|
121
|
+
"570F490D03FA",
|
|
122
|
+
],
|
|
123
|
+
"party": [
|
|
124
|
+
"570F490D01030400FF8A47FF524DFF4DEE",
|
|
125
|
+
"570F490D010304034DFF8C",
|
|
126
|
+
"570F490D03FA",
|
|
127
|
+
],
|
|
128
|
+
"dynamic": [
|
|
129
|
+
"570F490D010403004DFFFB4DFF4FFFBF4D",
|
|
130
|
+
"570F490D03FA",
|
|
131
|
+
],
|
|
132
|
+
"mystery": [
|
|
133
|
+
"570F490D01050300F660F6F6D460C6F660",
|
|
134
|
+
"570F490D03FA",
|
|
135
|
+
],
|
|
136
|
+
"lightning": [
|
|
137
|
+
"570F490D01340100FFD700",
|
|
138
|
+
"570F490D03FA",
|
|
139
|
+
],
|
|
140
|
+
"rock": [
|
|
141
|
+
"570F490D01090300B0F6606864FCFFBC3D",
|
|
142
|
+
"570F490D03FA",
|
|
143
|
+
],
|
|
144
|
+
"starlight": [
|
|
145
|
+
"570F490D010A0100FF8C00",
|
|
146
|
+
"570F490D0363",
|
|
147
|
+
],
|
|
148
|
+
"valentine_day": [
|
|
149
|
+
"570F490D010C0300FDE0FFFFCC8AD7FF8A",
|
|
150
|
+
"570F490D03FA",
|
|
151
|
+
],
|
|
152
|
+
"dream": [
|
|
153
|
+
"570F490D010E0300A3E5FF73F019FFA8E5",
|
|
154
|
+
"570F490D03FA",
|
|
155
|
+
],
|
|
156
|
+
"alarm": [
|
|
157
|
+
"570F490D013E0100FF0000",
|
|
158
|
+
"570F490D03FA",
|
|
159
|
+
],
|
|
160
|
+
"fireworks": [
|
|
161
|
+
"570F490D01110300FFAA33FFE233FF5CDF",
|
|
162
|
+
"570F490D03FA",
|
|
163
|
+
],
|
|
164
|
+
"waves": [
|
|
165
|
+
"570F490D013D01001E90FF",
|
|
166
|
+
"570F490D03FA",
|
|
167
|
+
],
|
|
168
|
+
"christmas": [
|
|
169
|
+
"570F490D01380400DC143C228B22DAA520",
|
|
170
|
+
"570F490D0363",
|
|
171
|
+
"570F490D0138040332CD32",
|
|
172
|
+
"570F490D0363",
|
|
173
|
+
],
|
|
174
|
+
"rainbow": [
|
|
175
|
+
"570F490D01160600FF0000FF7F00FFFF00",
|
|
176
|
+
"570F490D03FA",
|
|
177
|
+
"570F490D0116060300FF000000FF9400D3",
|
|
178
|
+
"570F490D03FA",
|
|
179
|
+
],
|
|
180
|
+
"game": [
|
|
181
|
+
"570F490D011A0400D05CFF668FFFFFEFD5",
|
|
182
|
+
"570F490D0363",
|
|
183
|
+
"570F490D011A0403FFC55C",
|
|
184
|
+
"570F490D0363",
|
|
185
|
+
],
|
|
186
|
+
"halloween": [
|
|
187
|
+
"570F490D01320300FF8C009370DB32CD32",
|
|
188
|
+
"570F490D0364",
|
|
189
|
+
],
|
|
190
|
+
"meditation": [
|
|
191
|
+
"570F490D013502001E90FF9370DB",
|
|
192
|
+
"570F490D0364",
|
|
193
|
+
],
|
|
194
|
+
"starlit_sky": [
|
|
195
|
+
"570F490D010D010099C8FF",
|
|
196
|
+
"570F490D0364",
|
|
197
|
+
],
|
|
198
|
+
"sleep": [
|
|
199
|
+
"570F490D01370300FF8C002E4E3E3E3E5E",
|
|
200
|
+
"570F490D0364",
|
|
201
|
+
],
|
|
202
|
+
"movie": [
|
|
203
|
+
"570F490D013602001919704B0082",
|
|
204
|
+
"570F490D0364",
|
|
205
|
+
],
|
|
206
|
+
"sunrise": [
|
|
207
|
+
"570F490D013F0200FFD700FF4500",
|
|
208
|
+
"570F490D03FA",
|
|
209
|
+
"570F490D03FA",
|
|
210
|
+
],
|
|
211
|
+
"sunset": [
|
|
212
|
+
"570F490D01390300FF4500FFA500483D8B",
|
|
213
|
+
"570F490D0363",
|
|
214
|
+
"570F490D0363",
|
|
215
|
+
],
|
|
216
|
+
"new_year": [
|
|
217
|
+
"570F490D013F0300FF0000FFD700228B22",
|
|
218
|
+
"570F490D0364",
|
|
219
|
+
],
|
|
220
|
+
"cherry_blossom": [
|
|
221
|
+
"570F490D01400200FFB3C1FF69B4",
|
|
222
|
+
"570F490D0364",
|
|
223
|
+
],
|
|
224
|
+
}
|
|
100
225
|
|
|
101
226
|
|
|
102
227
|
class SwitchbotLightStrip(SwitchbotSequenceBaseLight):
|
|
@@ -177,3 +302,44 @@ class SwitchbotStripLight3(SwitchbotEncryptedDevice, SwitchbotLightStrip):
|
|
|
177
302
|
def color_modes(self) -> set[ColorMode]:
|
|
178
303
|
"""Return the supported color modes."""
|
|
179
304
|
return {ColorMode.RGB, ColorMode.COLOR_TEMP}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class SwitchbotRgbicLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
|
|
308
|
+
"""Support for Switchbot RGBIC lights."""
|
|
309
|
+
|
|
310
|
+
_effect_dict = RGBIC_EFFECTS
|
|
311
|
+
|
|
312
|
+
def __init__(
|
|
313
|
+
self,
|
|
314
|
+
device: BLEDevice,
|
|
315
|
+
key_id: str,
|
|
316
|
+
encryption_key: str,
|
|
317
|
+
interface: int = 0,
|
|
318
|
+
model: SwitchbotModel = SwitchbotModel.RGBICWW_STRIP_LIGHT,
|
|
319
|
+
**kwargs: Any,
|
|
320
|
+
) -> None:
|
|
321
|
+
super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
|
|
322
|
+
|
|
323
|
+
@classmethod
|
|
324
|
+
async def verify_encryption_key(
|
|
325
|
+
cls,
|
|
326
|
+
device: BLEDevice,
|
|
327
|
+
key_id: str,
|
|
328
|
+
encryption_key: str,
|
|
329
|
+
model: SwitchbotModel = SwitchbotModel.RGBICWW_STRIP_LIGHT,
|
|
330
|
+
**kwargs: Any,
|
|
331
|
+
) -> bool:
|
|
332
|
+
return await super().verify_encryption_key(
|
|
333
|
+
device, key_id, encryption_key, model, **kwargs
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def color_modes(self) -> set[ColorMode]:
|
|
338
|
+
"""Return the supported color modes."""
|
|
339
|
+
return {ColorMode.RGB, ColorMode.COLOR_TEMP}
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def color_mode(self) -> ColorMode:
|
|
343
|
+
"""Return the current color mode."""
|
|
344
|
+
device_mode = RGBICStripLightColorMode(self._get_adv_value("color_mode") or 10)
|
|
345
|
+
return _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
|
|
@@ -128,6 +128,7 @@ class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
128
128
|
if self._model in (
|
|
129
129
|
SwitchbotModel.RELAY_SWITCH_1PM,
|
|
130
130
|
SwitchbotModel.RELAY_SWITCH_2PM,
|
|
131
|
+
SwitchbotModel.PLUG_MINI_EU,
|
|
131
132
|
):
|
|
132
133
|
if channel is None:
|
|
133
134
|
adv_data["voltage"] = self._get_adv_value("voltage") or 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Utility functions for switchbot."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@lru_cache(maxsize=512)
|
|
7
|
+
def format_mac_upper(mac: str) -> str:
|
|
8
|
+
"""Format the mac address string to uppercase with colons."""
|
|
9
|
+
to_test = mac
|
|
10
|
+
|
|
11
|
+
if len(to_test) == 17 and to_test.count(":") == 5:
|
|
12
|
+
return to_test.upper()
|
|
13
|
+
|
|
14
|
+
if len(to_test) == 17 and to_test.count("-") == 5:
|
|
15
|
+
to_test = to_test.replace("-", "")
|
|
16
|
+
elif len(to_test) == 14 and to_test.count(".") == 2:
|
|
17
|
+
to_test = to_test.replace(".", "")
|
|
18
|
+
|
|
19
|
+
if len(to_test) == 12:
|
|
20
|
+
# no : included
|
|
21
|
+
return ":".join(to_test.upper()[i : i + 2] for i in range(0, 12, 2))
|
|
22
|
+
|
|
23
|
+
# Not sure how formatted, return original
|
|
24
|
+
return mac.upper()
|