PySwitchbot 0.55.4__tar.gz → 0.56.1__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.55.4 → pyswitchbot-0.56.1}/PKG-INFO +11 -2
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/PySwitchbot.egg-info/PKG-INFO +11 -2
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/PySwitchbot.egg-info/SOURCES.txt +5 -1
- pyswitchbot-0.56.1/pyproject.toml +59 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/setup.py +7 -2
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/__init__.py +15 -16
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parser.py +15 -2
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/blind_tilt.py +0 -1
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/lock.py +1 -1
- pyswitchbot-0.56.1/switchbot/adv_parsers/remote.py +23 -0
- pyswitchbot-0.55.4/switchbot/const.py → pyswitchbot-0.56.1/switchbot/const/__init__.py +11 -16
- pyswitchbot-0.56.1/switchbot/const/lock.py +13 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/base_cover.py +2 -3
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/base_light.py +2 -2
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/curtain.py +0 -1
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/device.py +69 -9
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/lock.py +2 -58
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/relay_switch.py +0 -55
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/discovery.py +0 -1
- pyswitchbot-0.56.1/switchbot/helpers.py +17 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/tests/test_adv_parser.py +79 -1
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/tests/test_base_cover.py +0 -1
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/tests/test_blind_tilt.py +0 -1
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/tests/test_curtain.py +0 -1
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/tests/test_relay_switch.py +0 -1
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/LICENSE +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/MANIFEST.in +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/README.md +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/setup.cfg +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/enum.py +0 -0
- {pyswitchbot-0.55.4 → pyswitchbot-0.56.1}/switchbot/models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: PySwitchbot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.56.1
|
|
4
4
|
Summary: A library to communicate with Switchbot
|
|
5
5
|
Home-page: https://github.com/sblibs/pySwitchbot/
|
|
6
6
|
Author: Daniel Hjelseth Hoyer
|
|
@@ -20,6 +20,15 @@ Requires-Dist: bleak>=0.19.0
|
|
|
20
20
|
Requires-Dist: bleak-retry-connector>=3.4.0
|
|
21
21
|
Requires-Dist: cryptography>=39.0.0
|
|
22
22
|
Requires-Dist: pyOpenSSL>=23.0.0
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
23
32
|
|
|
24
33
|
# pySwitchbot [](https://travis-ci.org/sblibs/pySwitchbot)
|
|
25
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: PySwitchbot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.56.1
|
|
4
4
|
Summary: A library to communicate with Switchbot
|
|
5
5
|
Home-page: https://github.com/sblibs/pySwitchbot/
|
|
6
6
|
Author: Daniel Hjelseth Hoyer
|
|
@@ -20,6 +20,15 @@ Requires-Dist: bleak>=0.19.0
|
|
|
20
20
|
Requires-Dist: bleak-retry-connector>=3.4.0
|
|
21
21
|
Requires-Dist: cryptography>=39.0.0
|
|
22
22
|
Requires-Dist: pyOpenSSL>=23.0.0
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
23
32
|
|
|
24
33
|
# pySwitchbot [](https://travis-ci.org/sblibs/pySwitchbot)
|
|
25
34
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
MANIFEST.in
|
|
3
3
|
README.md
|
|
4
|
+
pyproject.toml
|
|
4
5
|
setup.py
|
|
5
6
|
PySwitchbot.egg-info/PKG-INFO
|
|
6
7
|
PySwitchbot.egg-info/SOURCES.txt
|
|
@@ -10,9 +11,9 @@ PySwitchbot.egg-info/top_level.txt
|
|
|
10
11
|
switchbot/__init__.py
|
|
11
12
|
switchbot/adv_parser.py
|
|
12
13
|
switchbot/api_config.py
|
|
13
|
-
switchbot/const.py
|
|
14
14
|
switchbot/discovery.py
|
|
15
15
|
switchbot/enum.py
|
|
16
|
+
switchbot/helpers.py
|
|
16
17
|
switchbot/models.py
|
|
17
18
|
switchbot/adv_parsers/__init__.py
|
|
18
19
|
switchbot/adv_parsers/blind_tilt.py
|
|
@@ -31,6 +32,9 @@ switchbot/adv_parsers/meter.py
|
|
|
31
32
|
switchbot/adv_parsers/motion.py
|
|
32
33
|
switchbot/adv_parsers/plug.py
|
|
33
34
|
switchbot/adv_parsers/relay_switch.py
|
|
35
|
+
switchbot/adv_parsers/remote.py
|
|
36
|
+
switchbot/const/__init__.py
|
|
37
|
+
switchbot/const/lock.py
|
|
34
38
|
switchbot/devices/__init__.py
|
|
35
39
|
switchbot/devices/base_cover.py
|
|
36
40
|
switchbot/devices/base_light.py
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[tool.ruff]
|
|
2
|
+
target-version = "py311"
|
|
3
|
+
line-length = 88
|
|
4
|
+
|
|
5
|
+
[tool.ruff.lint]
|
|
6
|
+
ignore = [
|
|
7
|
+
"S101", # use of assert
|
|
8
|
+
"D203", # 1 blank line required before class docstring
|
|
9
|
+
"D212", # Multi-line docstring summary should start at the first line
|
|
10
|
+
"D100", # Missing docstring in public module
|
|
11
|
+
"D101", # Missing docstring in public module
|
|
12
|
+
"D102", # Missing docstring in public method
|
|
13
|
+
"D103", # Missing docstring in public module
|
|
14
|
+
"D104", # Missing docstring in public package
|
|
15
|
+
"D105", # Missing docstring in magic method
|
|
16
|
+
"D107", # Missing docstring in `__init__`
|
|
17
|
+
"D400", # First line should end with a period
|
|
18
|
+
"D401", # First line of docstring should be in imperative mood
|
|
19
|
+
"D205", # 1 blank line required between summary line and description
|
|
20
|
+
"D415", # First line should end with a period, question mark, or exclamation point
|
|
21
|
+
"D417", # Missing argument descriptions in the docstring
|
|
22
|
+
"E501", # Line too long
|
|
23
|
+
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
|
24
|
+
"B008", # Do not perform function call
|
|
25
|
+
"S110", # `try`-`except`-`pass` detected, consider logging the exception
|
|
26
|
+
"D106", # Missing docstring in public nested class
|
|
27
|
+
"UP007", # typer needs Optional syntax
|
|
28
|
+
"UP038", # Use `X | Y` in `isinstance` is slower
|
|
29
|
+
"S603", # check for execution of untrusted input
|
|
30
|
+
"S105", # possible hard coded creds
|
|
31
|
+
]
|
|
32
|
+
select = [
|
|
33
|
+
"B", # flake8-bugbear
|
|
34
|
+
"D", # flake8-docstrings
|
|
35
|
+
"C4", # flake8-comprehensions
|
|
36
|
+
"S", # flake8-bandit
|
|
37
|
+
"F", # pyflake
|
|
38
|
+
"E", # pycodestyle
|
|
39
|
+
"W", # pycodestyle
|
|
40
|
+
"UP", # pyupgrade
|
|
41
|
+
"I", # isort
|
|
42
|
+
"RUF", # ruff specific
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint.per-file-ignores]
|
|
46
|
+
"tests/**/*" = [
|
|
47
|
+
"D100",
|
|
48
|
+
"D101",
|
|
49
|
+
"D102",
|
|
50
|
+
"D103",
|
|
51
|
+
"D104",
|
|
52
|
+
"S101",
|
|
53
|
+
]
|
|
54
|
+
"setup.py" = ["D100"]
|
|
55
|
+
"conftest.py" = ["D100"]
|
|
56
|
+
"docs/conf.py" = ["D100"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.isort]
|
|
59
|
+
known-first-party = ["pySwitchbot", "tests"]
|
|
@@ -7,7 +7,12 @@ long_description = (this_directory / "README.md").read_text()
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name="PySwitchbot",
|
|
10
|
-
packages=[
|
|
10
|
+
packages=[
|
|
11
|
+
"switchbot",
|
|
12
|
+
"switchbot.devices",
|
|
13
|
+
"switchbot.const",
|
|
14
|
+
"switchbot.adv_parsers",
|
|
15
|
+
],
|
|
11
16
|
install_requires=[
|
|
12
17
|
"aiohttp>=3.9.5",
|
|
13
18
|
"bleak>=0.19.0",
|
|
@@ -15,7 +20,7 @@ setup(
|
|
|
15
20
|
"cryptography>=39.0.0",
|
|
16
21
|
"pyOpenSSL>=23.0.0",
|
|
17
22
|
],
|
|
18
|
-
version="0.
|
|
23
|
+
version="0.56.1",
|
|
19
24
|
description="A library to communicate with Switchbot",
|
|
20
25
|
long_description=long_description,
|
|
21
26
|
long_description_content_type="text/markdown",
|
|
@@ -16,14 +16,13 @@ from .const import (
|
|
|
16
16
|
SwitchbotAuthenticationError,
|
|
17
17
|
SwitchbotModel,
|
|
18
18
|
)
|
|
19
|
-
from .devices.device import SwitchbotEncryptedDevice
|
|
20
19
|
from .devices.base_light import SwitchbotBaseLight
|
|
21
20
|
from .devices.blind_tilt import SwitchbotBlindTilt
|
|
22
21
|
from .devices.bot import Switchbot
|
|
23
22
|
from .devices.bulb import SwitchbotBulb
|
|
24
23
|
from .devices.ceiling_light import SwitchbotCeilingLight
|
|
25
24
|
from .devices.curtain import SwitchbotCurtain
|
|
26
|
-
from .devices.device import ColorMode, SwitchbotDevice
|
|
25
|
+
from .devices.device import ColorMode, SwitchbotDevice, SwitchbotEncryptedDevice
|
|
27
26
|
from .devices.humidifier import SwitchbotHumidifier
|
|
28
27
|
from .devices.light_strip import SwitchbotLightStrip
|
|
29
28
|
from .devices.lock import SwitchbotLock
|
|
@@ -33,30 +32,30 @@ from .discovery import GetSwitchbotDevices
|
|
|
33
32
|
from .models import SwitchBotAdvertisement
|
|
34
33
|
|
|
35
34
|
__all__ = [
|
|
36
|
-
"
|
|
37
|
-
"close_stale_connections",
|
|
38
|
-
"close_stale_connections_by_address",
|
|
39
|
-
"parse_advertisement_data",
|
|
35
|
+
"ColorMode",
|
|
40
36
|
"GetSwitchbotDevices",
|
|
37
|
+
"LockStatus",
|
|
41
38
|
"SwitchBotAdvertisement",
|
|
39
|
+
"Switchbot",
|
|
42
40
|
"SwitchbotAccountConnectionError",
|
|
43
41
|
"SwitchbotApiError",
|
|
44
42
|
"SwitchbotAuthenticationError",
|
|
45
|
-
"SwitchbotEncryptedDevice",
|
|
46
|
-
"ColorMode",
|
|
47
|
-
"LockStatus",
|
|
48
43
|
"SwitchbotBaseLight",
|
|
44
|
+
"SwitchbotBlindTilt",
|
|
49
45
|
"SwitchbotBulb",
|
|
50
46
|
"SwitchbotCeilingLight",
|
|
51
|
-
"SwitchbotDevice",
|
|
52
47
|
"SwitchbotCurtain",
|
|
53
|
-
"
|
|
48
|
+
"SwitchbotDevice",
|
|
49
|
+
"SwitchbotEncryptedDevice",
|
|
54
50
|
"SwitchbotHumidifier",
|
|
55
|
-
"
|
|
56
|
-
"SwitchbotPlugMini",
|
|
57
|
-
"SwitchbotSupportedType",
|
|
58
|
-
"SwitchbotModel",
|
|
51
|
+
"SwitchbotLightStrip",
|
|
59
52
|
"SwitchbotLock",
|
|
60
|
-
"
|
|
53
|
+
"SwitchbotModel",
|
|
54
|
+
"SwitchbotPlugMini",
|
|
61
55
|
"SwitchbotRelaySwitch",
|
|
56
|
+
"SwitchbotSupportedType",
|
|
57
|
+
"close_stale_connections",
|
|
58
|
+
"close_stale_connections_by_address",
|
|
59
|
+
"get_device",
|
|
60
|
+
"parse_advertisement_data",
|
|
62
61
|
]
|
|
@@ -29,6 +29,7 @@ from .adv_parsers.relay_switch import (
|
|
|
29
29
|
process_worelay_switch_1,
|
|
30
30
|
process_worelay_switch_1pm,
|
|
31
31
|
)
|
|
32
|
+
from .adv_parsers.remote import process_woremote
|
|
32
33
|
from .const import SwitchbotModel
|
|
33
34
|
from .models import SwitchBotAdvertisement
|
|
34
35
|
|
|
@@ -40,6 +41,8 @@ SERVICE_DATA_ORDER = (
|
|
|
40
41
|
)
|
|
41
42
|
MFR_DATA_ORDER = (2409, 741, 89)
|
|
42
43
|
|
|
44
|
+
APPLE_MANUFACTURER_ID = 76
|
|
45
|
+
|
|
43
46
|
|
|
44
47
|
class SwitchbotSupportedType(TypedDict):
|
|
45
48
|
"""Supported type of Switchbot."""
|
|
@@ -203,6 +206,12 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
|
|
|
203
206
|
"func": process_worelay_switch_1,
|
|
204
207
|
"manufacturer_id": 2409,
|
|
205
208
|
},
|
|
209
|
+
"b": {
|
|
210
|
+
"modelName": SwitchbotModel.REMOTE,
|
|
211
|
+
"modelFriendlyName": "Remote",
|
|
212
|
+
"func": process_woremote,
|
|
213
|
+
"manufacturer_id": 89,
|
|
214
|
+
},
|
|
206
215
|
}
|
|
207
216
|
|
|
208
217
|
_SWITCHBOT_MODEL_TO_CHAR = {
|
|
@@ -235,10 +244,14 @@ def parse_advertisement_data(
|
|
|
235
244
|
|
|
236
245
|
_mfr_data = None
|
|
237
246
|
_mfr_id = None
|
|
247
|
+
manufacturer_data = advertisement_data.manufacturer_data
|
|
248
|
+
if APPLE_MANUFACTURER_ID in manufacturer_data:
|
|
249
|
+
return None
|
|
250
|
+
|
|
238
251
|
for mfr_id in MFR_DATA_ORDER:
|
|
239
|
-
if mfr_id in
|
|
252
|
+
if mfr_id in manufacturer_data:
|
|
240
253
|
_mfr_id = mfr_id
|
|
241
|
-
_mfr_data =
|
|
254
|
+
_mfr_data = manufacturer_data[mfr_id]
|
|
242
255
|
break
|
|
243
256
|
|
|
244
257
|
if _mfr_data is None and _service_data is None:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Remote adv parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
_LOGGER = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def process_woremote(
|
|
11
|
+
data: bytes | None, mfr_data: bytes | None
|
|
12
|
+
) -> dict[str, int | None]:
|
|
13
|
+
"""Process WoRemote adv data."""
|
|
14
|
+
if data is None:
|
|
15
|
+
return {
|
|
16
|
+
"battery": None,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_LOGGER.debug("data: %s", data.hex())
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"battery": data[2] & 0b01111111,
|
|
23
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Switchbot Device Consts Library."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from enum import
|
|
5
|
+
from ..enum import StrEnum
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Preserve old LockStatus export for backwards compatibility
|
|
8
|
+
from .lock import LockStatus as LockStatus
|
|
8
9
|
|
|
9
10
|
DEFAULT_RETRY_COUNT = 3
|
|
10
11
|
DEFAULT_RETRY_TIMEOUT = 1
|
|
@@ -12,7 +13,8 @@ DEFAULT_SCAN_TIMEOUT = 5
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class SwitchbotApiError(RuntimeError):
|
|
15
|
-
"""
|
|
16
|
+
"""
|
|
17
|
+
Raised when API call fails.
|
|
16
18
|
|
|
17
19
|
This exception inherits from RuntimeError to avoid breaking existing code
|
|
18
20
|
but will be changed to Exception in a future release.
|
|
@@ -20,7 +22,8 @@ class SwitchbotApiError(RuntimeError):
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class SwitchbotAuthenticationError(RuntimeError):
|
|
23
|
-
"""
|
|
25
|
+
"""
|
|
26
|
+
Raised when authentication fails.
|
|
24
27
|
|
|
25
28
|
This exception inherits from RuntimeError to avoid breaking existing code
|
|
26
29
|
but will be changed to Exception in a future release.
|
|
@@ -28,7 +31,8 @@ class SwitchbotAuthenticationError(RuntimeError):
|
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class SwitchbotAccountConnectionError(RuntimeError):
|
|
31
|
-
"""
|
|
34
|
+
"""
|
|
35
|
+
Raised when connection to Switchbot account fails.
|
|
32
36
|
|
|
33
37
|
This exception inherits from RuntimeError to avoid breaking existing code
|
|
34
38
|
but will be changed to Exception in a future release.
|
|
@@ -57,13 +61,4 @@ class SwitchbotModel(StrEnum):
|
|
|
57
61
|
KEYPAD = "WoKeypad"
|
|
58
62
|
RELAY_SWITCH_1PM = "Relay Switch 1PM"
|
|
59
63
|
RELAY_SWITCH_1 = "Relay Switch 1"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class LockStatus(Enum):
|
|
63
|
-
LOCKED = 0
|
|
64
|
-
UNLOCKED = 1
|
|
65
|
-
LOCKING = 2
|
|
66
|
-
UNLOCKING = 3
|
|
67
|
-
LOCKING_STOP = 4 # LOCKING_BLOCKED
|
|
68
|
-
UNLOCKING_STOP = 5 # UNLOCKING_BLOCKED
|
|
69
|
-
NOT_FULLY_LOCKED = 6 # LATCH_LOCKED - Only EU lock type
|
|
64
|
+
REMOTE = "WoRemote"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LockStatus(Enum):
|
|
7
|
+
LOCKED = 0
|
|
8
|
+
UNLOCKED = 1
|
|
9
|
+
LOCKING = 2
|
|
10
|
+
UNLOCKING = 3
|
|
11
|
+
LOCKING_STOP = 4 # LOCKING_BLOCKED
|
|
12
|
+
UNLOCKING_STOP = 5 # UNLOCKING_BLOCKED
|
|
13
|
+
NOT_FULLY_LOCKED = 6 # LATCH_LOCKED - Only EU lock type
|
|
@@ -33,7 +33,6 @@ class SwitchbotBaseCover(SwitchbotDevice):
|
|
|
33
33
|
|
|
34
34
|
def __init__(self, reverse: bool, *args: Any, **kwargs: Any) -> None:
|
|
35
35
|
"""Switchbot Cover device constructor."""
|
|
36
|
-
|
|
37
36
|
super().__init__(*args, **kwargs)
|
|
38
37
|
self._reverse = reverse
|
|
39
38
|
self._settings: dict[str, Any] = {}
|
|
@@ -43,7 +42,8 @@ class SwitchbotBaseCover(SwitchbotDevice):
|
|
|
43
42
|
self._is_closing: bool = False
|
|
44
43
|
|
|
45
44
|
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
46
|
-
"""
|
|
45
|
+
"""
|
|
46
|
+
Send multiple commands to device.
|
|
47
47
|
|
|
48
48
|
Since we current have no way to tell which command the device
|
|
49
49
|
needs we send both.
|
|
@@ -84,7 +84,6 @@ class SwitchbotBaseCover(SwitchbotDevice):
|
|
|
84
84
|
|
|
85
85
|
async def get_extended_info_adv(self) -> dict[str, Any] | None:
|
|
86
86
|
"""Get advance page info for device chain."""
|
|
87
|
-
|
|
88
87
|
_data = await self._send_command(key=COVER_EXT_ADV_KEY)
|
|
89
88
|
if not _data:
|
|
90
89
|
_LOGGER.error("%s: Unsuccessful, no result from device", self.name)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import logging
|
|
5
4
|
import time
|
|
6
5
|
from abc import abstractmethod
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
8
|
+
from ..helpers import create_background_task
|
|
9
9
|
from ..models import SwitchBotAdvertisement
|
|
10
10
|
from .device import ColorMode, SwitchbotDevice
|
|
11
11
|
|
|
@@ -106,4 +106,4 @@ class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
|
|
|
106
106
|
new_state,
|
|
107
107
|
)
|
|
108
108
|
if current_state != new_state:
|
|
109
|
-
|
|
109
|
+
create_background_task(self.update())
|
|
@@ -38,7 +38,6 @@ class SwitchbotCurtain(SwitchbotBaseCover):
|
|
|
38
38
|
|
|
39
39
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
40
40
|
"""Switchbot Curtain/WoCurtain constructor."""
|
|
41
|
-
|
|
42
41
|
# The position of the curtain is saved returned with 0 = open and 100 = closed.
|
|
43
42
|
# This is independent of the calibration of the curtain bot (Open left to right/
|
|
44
43
|
# Open right to left/Open from the middle).
|
|
@@ -6,10 +6,10 @@ import asyncio
|
|
|
6
6
|
import binascii
|
|
7
7
|
import logging
|
|
8
8
|
import time
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from dataclasses import replace
|
|
10
11
|
from enum import Enum
|
|
11
12
|
from typing import Any, TypeVar, cast
|
|
12
|
-
from collections.abc import Callable
|
|
13
13
|
from uuid import UUID
|
|
14
14
|
|
|
15
15
|
import aiohttp
|
|
@@ -23,6 +23,7 @@ from bleak_retry_connector import (
|
|
|
23
23
|
ble_device_has_changed,
|
|
24
24
|
establish_connection,
|
|
25
25
|
)
|
|
26
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
26
27
|
|
|
27
28
|
from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_CLIENT_ID
|
|
28
29
|
from ..const import (
|
|
@@ -34,6 +35,7 @@ from ..const import (
|
|
|
34
35
|
SwitchbotModel,
|
|
35
36
|
)
|
|
36
37
|
from ..discovery import GetSwitchbotDevices
|
|
38
|
+
from ..helpers import create_background_task
|
|
37
39
|
from ..models import SwitchBotAdvertisement
|
|
38
40
|
|
|
39
41
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -45,6 +47,7 @@ REQ_HEADER = "570f"
|
|
|
45
47
|
DEVICE_GET_BASIC_SETTINGS_KEY = "5702"
|
|
46
48
|
DEVICE_SET_MODE_KEY = "5703"
|
|
47
49
|
DEVICE_SET_EXTENDED_KEY = REQ_HEADER
|
|
50
|
+
COMMAND_GET_CK_IV = f"{REQ_HEADER}2103"
|
|
48
51
|
|
|
49
52
|
# Base key when encryption is set
|
|
50
53
|
KEY_PASSWORD_PREFIX = "571"
|
|
@@ -81,7 +84,6 @@ class SwitchbotOperationError(Exception):
|
|
|
81
84
|
|
|
82
85
|
def _sb_uuid(comms_type: str = "service") -> UUID | str:
|
|
83
86
|
"""Return Switchbot UUID."""
|
|
84
|
-
|
|
85
87
|
_uuid = {"tx": "002", "rx": "003", "service": "d00"}
|
|
86
88
|
|
|
87
89
|
if comms_type in _uuid:
|
|
@@ -167,8 +169,8 @@ class SwitchbotBaseDevice:
|
|
|
167
169
|
session: aiohttp.ClientSession,
|
|
168
170
|
subdomain: str,
|
|
169
171
|
path: str,
|
|
170
|
-
data: dict = None,
|
|
171
|
-
headers: dict = None,
|
|
172
|
+
data: dict | None = None,
|
|
173
|
+
headers: dict | None = None,
|
|
172
174
|
) -> dict:
|
|
173
175
|
url = f"https://{subdomain}.{SWITCHBOT_APP_API_BASE_URL}/{path}"
|
|
174
176
|
async with session.post(
|
|
@@ -637,7 +639,8 @@ class SwitchbotBaseDevice:
|
|
|
637
639
|
return result[index] in values
|
|
638
640
|
|
|
639
641
|
def _update_parsed_data(self, new_data: dict[str, Any]) -> bool:
|
|
640
|
-
"""
|
|
642
|
+
"""
|
|
643
|
+
Update data.
|
|
641
644
|
|
|
642
645
|
Returns true if data has changed and False if not.
|
|
643
646
|
"""
|
|
@@ -691,7 +694,8 @@ class SwitchbotBaseDevice:
|
|
|
691
694
|
|
|
692
695
|
|
|
693
696
|
class SwitchbotDevice(SwitchbotBaseDevice):
|
|
694
|
-
"""
|
|
697
|
+
"""
|
|
698
|
+
Base Representation of a Switchbot Device.
|
|
695
699
|
|
|
696
700
|
This base class consumes the advertisement data during connection. If the device
|
|
697
701
|
sends stale advertisement data while connected, use
|
|
@@ -827,9 +831,64 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
|
|
|
827
831
|
|
|
828
832
|
return info is not None
|
|
829
833
|
|
|
834
|
+
async def _send_command(
|
|
835
|
+
self, key: str, retry: int | None = None, encrypt: bool = True
|
|
836
|
+
) -> bytes | None:
|
|
837
|
+
if not encrypt:
|
|
838
|
+
return await super()._send_command(key[:2] + "000000" + key[2:], retry)
|
|
839
|
+
|
|
840
|
+
result = await self._ensure_encryption_initialized()
|
|
841
|
+
if not result:
|
|
842
|
+
_LOGGER.error("Failed to initialize encryption")
|
|
843
|
+
return None
|
|
844
|
+
|
|
845
|
+
encrypted = (
|
|
846
|
+
key[:2] + self._key_id + self._iv[0:2].hex() + self._encrypt(key[2:])
|
|
847
|
+
)
|
|
848
|
+
result = await super()._send_command(encrypted, retry)
|
|
849
|
+
return result[:1] + self._decrypt(result[4:])
|
|
850
|
+
|
|
851
|
+
async def _ensure_encryption_initialized(self) -> bool:
|
|
852
|
+
if self._iv is not None:
|
|
853
|
+
return True
|
|
854
|
+
|
|
855
|
+
result = await self._send_command(
|
|
856
|
+
COMMAND_GET_CK_IV + self._key_id, encrypt=False
|
|
857
|
+
)
|
|
858
|
+
ok = self._check_command_result(result, 0, {1})
|
|
859
|
+
if ok:
|
|
860
|
+
self._iv = result[4:]
|
|
861
|
+
|
|
862
|
+
return ok
|
|
863
|
+
|
|
864
|
+
async def _execute_disconnect(self) -> None:
|
|
865
|
+
await super()._execute_disconnect()
|
|
866
|
+
self._iv = None
|
|
867
|
+
self._cipher = None
|
|
868
|
+
|
|
869
|
+
def _get_cipher(self) -> Cipher:
|
|
870
|
+
if self._cipher is None:
|
|
871
|
+
self._cipher = Cipher(
|
|
872
|
+
algorithms.AES128(self._encryption_key), modes.CTR(self._iv)
|
|
873
|
+
)
|
|
874
|
+
return self._cipher
|
|
875
|
+
|
|
876
|
+
def _encrypt(self, data: str) -> str:
|
|
877
|
+
if len(data) == 0:
|
|
878
|
+
return ""
|
|
879
|
+
encryptor = self._get_cipher().encryptor()
|
|
880
|
+
return (encryptor.update(bytearray.fromhex(data)) + encryptor.finalize()).hex()
|
|
881
|
+
|
|
882
|
+
def _decrypt(self, data: bytearray) -> bytes:
|
|
883
|
+
if len(data) == 0:
|
|
884
|
+
return b""
|
|
885
|
+
decryptor = self._get_cipher().decryptor()
|
|
886
|
+
return decryptor.update(data) + decryptor.finalize()
|
|
887
|
+
|
|
830
888
|
|
|
831
889
|
class SwitchbotDeviceOverrideStateDuringConnection(SwitchbotBaseDevice):
|
|
832
|
-
"""
|
|
890
|
+
"""
|
|
891
|
+
Base Representation of a Switchbot Device.
|
|
833
892
|
|
|
834
893
|
This base class ignores the advertisement data during connection and uses the
|
|
835
894
|
data from the device instead.
|
|
@@ -847,7 +906,8 @@ class SwitchbotDeviceOverrideStateDuringConnection(SwitchbotBaseDevice):
|
|
|
847
906
|
|
|
848
907
|
|
|
849
908
|
class SwitchbotSequenceDevice(SwitchbotDevice):
|
|
850
|
-
"""
|
|
909
|
+
"""
|
|
910
|
+
A Switchbot sequence device.
|
|
851
911
|
|
|
852
912
|
This class must not use SwitchbotDeviceOverrideStateDuringConnection because
|
|
853
913
|
it needs to know when the sequence_number has changed.
|
|
@@ -866,4 +926,4 @@ class SwitchbotSequenceDevice(SwitchbotDevice):
|
|
|
866
926
|
new_state,
|
|
867
927
|
)
|
|
868
928
|
if current_state != new_state:
|
|
869
|
-
|
|
929
|
+
create_background_task(self.update())
|
|
@@ -7,13 +7,12 @@ import time
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from bleak.backends.device import BLEDevice
|
|
10
|
-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
11
10
|
|
|
12
|
-
from ..const import
|
|
11
|
+
from ..const import SwitchbotModel
|
|
12
|
+
from ..const.lock import LockStatus
|
|
13
13
|
from .device import SwitchbotEncryptedDevice
|
|
14
14
|
|
|
15
15
|
COMMAND_HEADER = "57"
|
|
16
|
-
COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
|
|
17
16
|
COMMAND_LOCK_INFO = {
|
|
18
17
|
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4f8101",
|
|
19
18
|
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4f8102",
|
|
@@ -220,58 +219,3 @@ class SwitchbotLock(SwitchbotEncryptedDevice):
|
|
|
220
219
|
"unclosed_alarm": bool(data[1] & 0b00100000),
|
|
221
220
|
"unlocked_alarm": bool(data[1] & 0b00010000),
|
|
222
221
|
}
|
|
223
|
-
|
|
224
|
-
async def _send_command(
|
|
225
|
-
self, key: str, retry: int | None = None, encrypt: bool = True
|
|
226
|
-
) -> bytes | None:
|
|
227
|
-
if not encrypt:
|
|
228
|
-
return await super()._send_command(key[:2] + "000000" + key[2:], retry)
|
|
229
|
-
|
|
230
|
-
result = await self._ensure_encryption_initialized()
|
|
231
|
-
if not result:
|
|
232
|
-
_LOGGER.error("Failed to initialize encryption")
|
|
233
|
-
return None
|
|
234
|
-
|
|
235
|
-
encrypted = (
|
|
236
|
-
key[:2] + self._key_id + self._iv[0:2].hex() + self._encrypt(key[2:])
|
|
237
|
-
)
|
|
238
|
-
result = await super()._send_command(encrypted, retry)
|
|
239
|
-
return result[:1] + self._decrypt(result[4:])
|
|
240
|
-
|
|
241
|
-
async def _ensure_encryption_initialized(self) -> bool:
|
|
242
|
-
if self._iv is not None:
|
|
243
|
-
return True
|
|
244
|
-
|
|
245
|
-
result = await self._send_command(
|
|
246
|
-
COMMAND_GET_CK_IV + self._key_id, encrypt=False
|
|
247
|
-
)
|
|
248
|
-
ok = self._check_command_result(result, 0, COMMAND_RESULT_EXPECTED_VALUES)
|
|
249
|
-
if ok:
|
|
250
|
-
self._iv = result[4:]
|
|
251
|
-
|
|
252
|
-
return ok
|
|
253
|
-
|
|
254
|
-
async def _execute_disconnect(self) -> None:
|
|
255
|
-
await super()._execute_disconnect()
|
|
256
|
-
self._iv = None
|
|
257
|
-
self._cipher = None
|
|
258
|
-
self._notifications_enabled = False
|
|
259
|
-
|
|
260
|
-
def _get_cipher(self) -> Cipher:
|
|
261
|
-
if self._cipher is None:
|
|
262
|
-
self._cipher = Cipher(
|
|
263
|
-
algorithms.AES128(self._encryption_key), modes.CTR(self._iv)
|
|
264
|
-
)
|
|
265
|
-
return self._cipher
|
|
266
|
-
|
|
267
|
-
def _encrypt(self, data: str) -> str:
|
|
268
|
-
if len(data) == 0:
|
|
269
|
-
return ""
|
|
270
|
-
encryptor = self._get_cipher().encryptor()
|
|
271
|
-
return (encryptor.update(bytearray.fromhex(data)) + encryptor.finalize()).hex()
|
|
272
|
-
|
|
273
|
-
def _decrypt(self, data: bytearray) -> bytes:
|
|
274
|
-
if len(data) == 0:
|
|
275
|
-
return b""
|
|
276
|
-
decryptor = self._get_cipher().decryptor()
|
|
277
|
-
return decryptor.update(data) + decryptor.finalize()
|
|
@@ -3,7 +3,6 @@ import time
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from bleak.backends.device import BLEDevice
|
|
6
|
-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
7
6
|
|
|
8
7
|
from ..const import SwitchbotModel
|
|
9
8
|
from ..models import SwitchBotAdvertisement
|
|
@@ -12,7 +11,6 @@ from .device import SwitchbotEncryptedDevice
|
|
|
12
11
|
_LOGGER = logging.getLogger(__name__)
|
|
13
12
|
|
|
14
13
|
COMMAND_HEADER = "57"
|
|
15
|
-
COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
|
|
16
14
|
COMMAND_TURN_OFF = f"{COMMAND_HEADER}0f70010000"
|
|
17
15
|
COMMAND_TURN_ON = f"{COMMAND_HEADER}0f70010100"
|
|
18
16
|
COMMAND_TOGGLE = f"{COMMAND_HEADER}0f70010200"
|
|
@@ -139,56 +137,3 @@ class SwitchbotRelaySwitch(SwitchbotEncryptedDevice):
|
|
|
139
137
|
def is_on(self) -> bool | None:
|
|
140
138
|
"""Return switch state from cache."""
|
|
141
139
|
return self._get_adv_value("isOn")
|
|
142
|
-
|
|
143
|
-
async def _send_command(
|
|
144
|
-
self, key: str, retry: int | None = None, encrypt: bool = True
|
|
145
|
-
) -> bytes | None:
|
|
146
|
-
if not encrypt:
|
|
147
|
-
return await super()._send_command(key[:2] + "000000" + key[2:], retry)
|
|
148
|
-
|
|
149
|
-
result = await self._ensure_encryption_initialized()
|
|
150
|
-
if not result:
|
|
151
|
-
return None
|
|
152
|
-
|
|
153
|
-
encrypted = (
|
|
154
|
-
key[:2] + self._key_id + self._iv[0:2].hex() + self._encrypt(key[2:])
|
|
155
|
-
)
|
|
156
|
-
result = await super()._send_command(encrypted, retry)
|
|
157
|
-
return result[:1] + self._decrypt(result[4:])
|
|
158
|
-
|
|
159
|
-
async def _ensure_encryption_initialized(self) -> bool:
|
|
160
|
-
if self._iv is not None:
|
|
161
|
-
return True
|
|
162
|
-
|
|
163
|
-
result = await self._send_command(
|
|
164
|
-
COMMAND_GET_CK_IV + self._key_id, encrypt=False
|
|
165
|
-
)
|
|
166
|
-
ok = self._check_command_result(result, 0, {1})
|
|
167
|
-
if ok:
|
|
168
|
-
self._iv = result[4:]
|
|
169
|
-
|
|
170
|
-
return ok
|
|
171
|
-
|
|
172
|
-
async def _execute_disconnect(self) -> None:
|
|
173
|
-
await super()._execute_disconnect()
|
|
174
|
-
self._iv = None
|
|
175
|
-
self._cipher = None
|
|
176
|
-
|
|
177
|
-
def _get_cipher(self) -> Cipher:
|
|
178
|
-
if self._cipher is None:
|
|
179
|
-
self._cipher = Cipher(
|
|
180
|
-
algorithms.AES128(self._encryption_key), modes.CTR(self._iv)
|
|
181
|
-
)
|
|
182
|
-
return self._cipher
|
|
183
|
-
|
|
184
|
-
def _encrypt(self, data: str) -> str:
|
|
185
|
-
if len(data) == 0:
|
|
186
|
-
return ""
|
|
187
|
-
encryptor = self._get_cipher().encryptor()
|
|
188
|
-
return (encryptor.update(bytearray.fromhex(data)) + encryptor.finalize()).hex()
|
|
189
|
-
|
|
190
|
-
def _decrypt(self, data: bytearray) -> bytes:
|
|
191
|
-
if len(data) == 0:
|
|
192
|
-
return b""
|
|
193
|
-
decryptor = self._get_cipher().decryptor()
|
|
194
|
-
return decryptor.update(data) + decryptor.finalize()
|
|
@@ -39,7 +39,6 @@ class GetSwitchbotDevices:
|
|
|
39
39
|
self, retry: int = DEFAULT_RETRY_COUNT, scan_timeout: int = DEFAULT_SCAN_TIMEOUT
|
|
40
40
|
) -> dict:
|
|
41
41
|
"""Find switchbot devices and their advertisement data."""
|
|
42
|
-
|
|
43
42
|
devices = None
|
|
44
43
|
devices = bleak.BleakScanner(
|
|
45
44
|
detection_callback=self.detection_callback,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Coroutine
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
_R = TypeVar("_R")
|
|
8
|
+
|
|
9
|
+
_BACKGROUND_TASKS: set[asyncio.Task[Any]] = set()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_background_task(target: Coroutine[Any, Any, _R]) -> asyncio.Task[_R]:
|
|
13
|
+
"""Create a background task."""
|
|
14
|
+
task = asyncio.create_task(target)
|
|
15
|
+
_BACKGROUND_TASKS.add(task)
|
|
16
|
+
task.add_done_callback(_BACKGROUND_TASKS.remove)
|
|
17
|
+
return task
|
|
@@ -5,8 +5,9 @@ from typing import Any
|
|
|
5
5
|
from bleak.backends.device import BLEDevice
|
|
6
6
|
from bleak.backends.scanner import AdvertisementData
|
|
7
7
|
|
|
8
|
-
from switchbot import
|
|
8
|
+
from switchbot import SwitchbotModel
|
|
9
9
|
from switchbot.adv_parser import parse_advertisement_data
|
|
10
|
+
from switchbot.const.lock import LockStatus
|
|
10
11
|
from switchbot.models import SwitchBotAdvertisement
|
|
11
12
|
|
|
12
13
|
ADVERTISEMENT_DATA_DEFAULTS = {
|
|
@@ -1917,3 +1918,80 @@ def test_leak_real_data_from_ha():
|
|
|
1917
1918
|
rssi=-73,
|
|
1918
1919
|
active=True,
|
|
1919
1920
|
)
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
def test_remote_active() -> None:
|
|
1924
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
1925
|
+
adv_data = generate_advertisement_data(
|
|
1926
|
+
manufacturer_data={89: b"\xaa\xbb\xcc\xdd\xee\xff"},
|
|
1927
|
+
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"b V\x00"},
|
|
1928
|
+
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
|
1929
|
+
rssi=-95,
|
|
1930
|
+
)
|
|
1931
|
+
result = parse_advertisement_data(ble_device, adv_data)
|
|
1932
|
+
assert result == SwitchBotAdvertisement(
|
|
1933
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
1934
|
+
data={
|
|
1935
|
+
"data": {
|
|
1936
|
+
"battery": 86,
|
|
1937
|
+
},
|
|
1938
|
+
"isEncrypted": False,
|
|
1939
|
+
"model": "b",
|
|
1940
|
+
"modelFriendlyName": "Remote",
|
|
1941
|
+
"modelName": SwitchbotModel.REMOTE,
|
|
1942
|
+
"rawAdvData": b"b V\x00",
|
|
1943
|
+
},
|
|
1944
|
+
device=ble_device,
|
|
1945
|
+
rssi=-95,
|
|
1946
|
+
active=True,
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
def test_remote_passive() -> None:
|
|
1951
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
1952
|
+
adv_data = generate_advertisement_data(
|
|
1953
|
+
manufacturer_data={89: b"\xaa\xbb\xcc\xdd\xee\xff"},
|
|
1954
|
+
rssi=-97,
|
|
1955
|
+
)
|
|
1956
|
+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.REMOTE)
|
|
1957
|
+
assert result == SwitchBotAdvertisement(
|
|
1958
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
1959
|
+
data={
|
|
1960
|
+
"data": {
|
|
1961
|
+
"battery": None,
|
|
1962
|
+
},
|
|
1963
|
+
"isEncrypted": False,
|
|
1964
|
+
"model": "b",
|
|
1965
|
+
"modelFriendlyName": "Remote",
|
|
1966
|
+
"modelName": SwitchbotModel.REMOTE,
|
|
1967
|
+
"rawAdvData": None,
|
|
1968
|
+
},
|
|
1969
|
+
device=ble_device,
|
|
1970
|
+
rssi=-97,
|
|
1971
|
+
active=False,
|
|
1972
|
+
)
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
def test_parse_advertisement_ignores_devices_with_apple_manufacturer_id():
|
|
1976
|
+
"""Test parse_advertisement_data ignores devices with apple manufacturer id."""
|
|
1977
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
1978
|
+
adv_data = generate_advertisement_data(
|
|
1979
|
+
local_name="WoCurtain",
|
|
1980
|
+
manufacturer_data={
|
|
1981
|
+
89: b"\xcc\xf4\xc4\xf9\xacl",
|
|
1982
|
+
2409: b"\xcc\xf4\xc4\xf9\xacl\xe2\x0f\x00\x12\x04",
|
|
1983
|
+
76: b"\x10",
|
|
1984
|
+
},
|
|
1985
|
+
service_data={
|
|
1986
|
+
"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Yd\x11\x04",
|
|
1987
|
+
"0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0d\x00\x12\x04",
|
|
1988
|
+
},
|
|
1989
|
+
service_uuids=[
|
|
1990
|
+
"00001800-0000-1000-8000-00805f9b34fb",
|
|
1991
|
+
"00001801-0000-1000-8000-00805f9b34fb",
|
|
1992
|
+
"cba20d00-224d-11e6-9fb8-0002a5d5c51b",
|
|
1993
|
+
],
|
|
1994
|
+
rssi=-2,
|
|
1995
|
+
)
|
|
1996
|
+
result = parse_advertisement_data(ble_device, adv_data)
|
|
1997
|
+
assert result is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|