PySwitchbot 0.45.0__tar.gz → 0.46.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.45.0 → pyswitchbot-0.46.1}/PKG-INFO +2 -3
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/PySwitchbot.egg-info/PKG-INFO +2 -3
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/PySwitchbot.egg-info/requires.txt +1 -2
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/setup.py +2 -3
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/__init__.py +2 -0
- pyswitchbot-0.46.1/switchbot/api_config.py +6 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/const.py +8 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/base_cover.py +0 -1
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/lock.py +82 -60
- PySwitchbot-0.45.0/switchbot/api_config.py +0 -13
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/LICENSE +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/MANIFEST.in +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/PySwitchbot.egg-info/SOURCES.txt +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/PySwitchbot.egg-info/top_level.txt +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/README.md +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/setup.cfg +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parser.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/__init__.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/bot.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/bulb.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/contact.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/curtain.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/humidifier.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/light_strip.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/lock.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/meter.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/motion.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/adv_parsers/plug.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/__init__.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/base_light.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/blind_tilt.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/bot.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/bulb.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/ceiling_light.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/contact.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/curtain.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/device.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/humidifier.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/light_strip.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/meter.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/motion.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/devices/plug.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/discovery.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/enum.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/switchbot/models.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/tests/test_adv_parser.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/tests/test_base_cover.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/tests/test_blind_tilt.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.1}/tests/test_curtain.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PySwitchbot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.46.1
|
|
4
4
|
Summary: A library to communicate with Switchbot
|
|
5
5
|
Home-page: https://github.com/Danielhiversen/pySwitchbot/
|
|
6
6
|
Author: Daniel Hjelseth Hoyer
|
|
@@ -13,9 +13,8 @@ Classifier: Programming Language :: Python
|
|
|
13
13
|
Classifier: Topic :: Home Automation
|
|
14
14
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Requires-Dist: aiohttp>=3.9.5
|
|
16
17
|
Requires-Dist: bleak>=0.19.0
|
|
17
18
|
Requires-Dist: bleak-retry-connector>=3.4.0
|
|
18
19
|
Requires-Dist: cryptography>=39.0.0
|
|
19
20
|
Requires-Dist: pyOpenSSL>=23.0.0
|
|
20
|
-
Requires-Dist: boto3>=1.20.24
|
|
21
|
-
Requires-Dist: requests>=2.28.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PySwitchbot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.46.1
|
|
4
4
|
Summary: A library to communicate with Switchbot
|
|
5
5
|
Home-page: https://github.com/Danielhiversen/pySwitchbot/
|
|
6
6
|
Author: Daniel Hjelseth Hoyer
|
|
@@ -13,9 +13,8 @@ Classifier: Programming Language :: Python
|
|
|
13
13
|
Classifier: Topic :: Home Automation
|
|
14
14
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Requires-Dist: aiohttp>=3.9.5
|
|
16
17
|
Requires-Dist: bleak>=0.19.0
|
|
17
18
|
Requires-Dist: bleak-retry-connector>=3.4.0
|
|
18
19
|
Requires-Dist: cryptography>=39.0.0
|
|
19
20
|
Requires-Dist: pyOpenSSL>=23.0.0
|
|
20
|
-
Requires-Dist: boto3>=1.20.24
|
|
21
|
-
Requires-Dist: requests>=2.28.1
|
|
@@ -4,14 +4,13 @@ setup(
|
|
|
4
4
|
name="PySwitchbot",
|
|
5
5
|
packages=["switchbot", "switchbot.devices", "switchbot.adv_parsers"],
|
|
6
6
|
install_requires=[
|
|
7
|
+
"aiohttp>=3.9.5",
|
|
7
8
|
"bleak>=0.19.0",
|
|
8
9
|
"bleak-retry-connector>=3.4.0",
|
|
9
10
|
"cryptography>=39.0.0",
|
|
10
11
|
"pyOpenSSL>=23.0.0",
|
|
11
|
-
"boto3>=1.20.24",
|
|
12
|
-
"requests>=2.28.1",
|
|
13
12
|
],
|
|
14
|
-
version="0.
|
|
13
|
+
version="0.46.1",
|
|
15
14
|
description="A library to communicate with Switchbot",
|
|
16
15
|
author="Daniel Hjelseth Hoyer",
|
|
17
16
|
url="https://github.com/Danielhiversen/pySwitchbot/",
|
|
@@ -11,6 +11,7 @@ from .adv_parser import SwitchbotSupportedType, parse_advertisement_data
|
|
|
11
11
|
from .const import (
|
|
12
12
|
LockStatus,
|
|
13
13
|
SwitchbotAccountConnectionError,
|
|
14
|
+
SwitchbotApiError,
|
|
14
15
|
SwitchbotAuthenticationError,
|
|
15
16
|
SwitchbotModel,
|
|
16
17
|
)
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"GetSwitchbotDevices",
|
|
37
38
|
"SwitchBotAdvertisement",
|
|
38
39
|
"SwitchbotAccountConnectionError",
|
|
40
|
+
"SwitchbotApiError",
|
|
39
41
|
"SwitchbotAuthenticationError",
|
|
40
42
|
"ColorMode",
|
|
41
43
|
"LockStatus",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Those values have been obtained from the following files in SwitchBot Android app
|
|
2
|
+
# That's how you can verify them yourself
|
|
3
|
+
# /assets/switchbot_config.json
|
|
4
|
+
|
|
5
|
+
SWITCHBOT_APP_API_BASE_URL = "api.switchbot.net"
|
|
6
|
+
SWITCHBOT_APP_CLIENT_ID = "5nnwmhmsa9xxskm14hd85lm9bm"
|
|
@@ -10,6 +10,14 @@ DEFAULT_RETRY_TIMEOUT = 1
|
|
|
10
10
|
DEFAULT_SCAN_TIMEOUT = 5
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class SwitchbotApiError(RuntimeError):
|
|
14
|
+
"""Raised when API call fails.
|
|
15
|
+
|
|
16
|
+
This exception inherits from RuntimeError to avoid breaking existing code
|
|
17
|
+
but will be changed to Exception in a future release.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
13
21
|
class SwitchbotAuthenticationError(RuntimeError):
|
|
14
22
|
"""Raised when authentication fails.
|
|
15
23
|
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
"""Library to handle connection with Switchbot Lock."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import hashlib
|
|
6
|
-
import hmac
|
|
7
|
-
import json
|
|
4
|
+
import asyncio
|
|
8
5
|
import logging
|
|
9
6
|
import time
|
|
10
7
|
from typing import Any
|
|
11
8
|
|
|
12
|
-
import
|
|
13
|
-
import requests
|
|
9
|
+
import aiohttp
|
|
14
10
|
from bleak.backends.device import BLEDevice
|
|
15
11
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
16
12
|
|
|
17
|
-
from ..api_config import SWITCHBOT_APP_API_BASE_URL,
|
|
13
|
+
from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_CLIENT_ID
|
|
18
14
|
from ..const import (
|
|
19
15
|
LockStatus,
|
|
20
16
|
SwitchbotAccountConnectionError,
|
|
17
|
+
SwitchbotApiError,
|
|
21
18
|
SwitchbotAuthenticationError,
|
|
22
19
|
)
|
|
23
20
|
from .device import SwitchbotDevice, SwitchbotOperationError
|
|
@@ -86,77 +83,102 @@ class SwitchbotLock(SwitchbotDevice):
|
|
|
86
83
|
|
|
87
84
|
return lock_info is not None
|
|
88
85
|
|
|
86
|
+
@staticmethod
|
|
87
|
+
async def api_request(
|
|
88
|
+
session: aiohttp.ClientSession,
|
|
89
|
+
subdomain: str,
|
|
90
|
+
path: str,
|
|
91
|
+
data: dict = None,
|
|
92
|
+
headers: dict = None,
|
|
93
|
+
) -> dict:
|
|
94
|
+
url = f"https://{subdomain}.{SWITCHBOT_APP_API_BASE_URL}/{path}"
|
|
95
|
+
async with session.post(
|
|
96
|
+
url,
|
|
97
|
+
json=data,
|
|
98
|
+
headers=headers,
|
|
99
|
+
timeout=aiohttp.ClientTimeout(total=10),
|
|
100
|
+
) as result:
|
|
101
|
+
if result.status > 299:
|
|
102
|
+
raise SwitchbotApiError(
|
|
103
|
+
f"Unexpected status code returned by SwitchBot API: {result.status}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
response = await result.json()
|
|
107
|
+
if response["statusCode"] != 100:
|
|
108
|
+
raise SwitchbotApiError(
|
|
109
|
+
f"{response['message']}, status code: {response['statusCode']}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return response["body"]
|
|
113
|
+
|
|
114
|
+
# Old non-async method preserved for backwards compatibility
|
|
89
115
|
@staticmethod
|
|
90
116
|
def retrieve_encryption_key(device_mac: str, username: str, password: str):
|
|
117
|
+
async def async_fn():
|
|
118
|
+
async with aiohttp.ClientSession() as session:
|
|
119
|
+
return await SwitchbotLock.async_retrieve_encryption_key(
|
|
120
|
+
session, device_mac, username, password
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return asyncio.run(async_fn())
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
async def async_retrieve_encryption_key(
|
|
127
|
+
session: aiohttp.ClientSession, device_mac: str, username: str, password: str
|
|
128
|
+
) -> dict:
|
|
91
129
|
"""Retrieve lock key from internal SwitchBot API."""
|
|
92
130
|
device_mac = device_mac.replace(":", "").replace("-", "").upper()
|
|
93
|
-
|
|
94
|
-
secret_hash = base64.b64encode(
|
|
95
|
-
hmac.new(
|
|
96
|
-
SWITCHBOT_APP_COGNITO_POOL["AppClientSecret"].encode(),
|
|
97
|
-
msg,
|
|
98
|
-
digestmod=hashlib.sha256,
|
|
99
|
-
).digest()
|
|
100
|
-
).decode()
|
|
101
|
-
|
|
102
|
-
cognito_idp_client = boto3.client(
|
|
103
|
-
"cognito-idp", region_name=SWITCHBOT_APP_COGNITO_POOL["Region"]
|
|
104
|
-
)
|
|
131
|
+
|
|
105
132
|
try:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
"
|
|
133
|
+
auth_result = await SwitchbotLock.api_request(
|
|
134
|
+
session,
|
|
135
|
+
"account",
|
|
136
|
+
"account/api/v1/user/login",
|
|
137
|
+
{
|
|
138
|
+
"clientId": SWITCHBOT_APP_CLIENT_ID,
|
|
139
|
+
"username": username,
|
|
140
|
+
"password": password,
|
|
141
|
+
"grantType": "password",
|
|
142
|
+
"verifyCode": "",
|
|
113
143
|
},
|
|
114
144
|
)
|
|
115
|
-
|
|
116
|
-
raise SwitchbotAuthenticationError(
|
|
117
|
-
f"Failed to authenticate: {err}"
|
|
118
|
-
) from err
|
|
145
|
+
auth_headers = {"authorization": auth_result["access_token"]}
|
|
119
146
|
except Exception as err:
|
|
120
|
-
raise SwitchbotAuthenticationError(
|
|
121
|
-
f"Unexpected error during authentication: {err}"
|
|
122
|
-
) from err
|
|
147
|
+
raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err
|
|
123
148
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
149
|
+
try:
|
|
150
|
+
userinfo = await SwitchbotLock.api_request(
|
|
151
|
+
session, "account", "account/api/v1/user/userinfo", {}, auth_headers
|
|
152
|
+
)
|
|
153
|
+
if "botRegion" in userinfo and userinfo["botRegion"] != "":
|
|
154
|
+
region = userinfo["botRegion"]
|
|
155
|
+
else:
|
|
156
|
+
region = "us"
|
|
157
|
+
except Exception as err:
|
|
158
|
+
raise SwitchbotAccountConnectionError(
|
|
159
|
+
f"Failed to retrieve SwitchBot Account user details: {err}"
|
|
160
|
+
) from err
|
|
130
161
|
|
|
131
|
-
access_token = auth_response["AuthenticationResult"]["AccessToken"]
|
|
132
162
|
try:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
device_info = await SwitchbotLock.api_request(
|
|
164
|
+
session,
|
|
165
|
+
f"wonderlabs.{region}",
|
|
166
|
+
"wonder/keys/v1/communicate",
|
|
167
|
+
{
|
|
137
168
|
"device_mac": device_mac,
|
|
138
169
|
"keyType": "user",
|
|
139
170
|
},
|
|
140
|
-
|
|
171
|
+
auth_headers,
|
|
141
172
|
)
|
|
142
|
-
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"key_id": device_info["communicationKey"]["keyId"],
|
|
176
|
+
"encryption_key": device_info["communicationKey"]["key"],
|
|
177
|
+
}
|
|
178
|
+
except Exception as err:
|
|
143
179
|
raise SwitchbotAccountConnectionError(
|
|
144
180
|
f"Failed to retrieve encryption key from SwitchBot Account: {err}"
|
|
145
181
|
) from err
|
|
146
|
-
if key_response.status_code > 299:
|
|
147
|
-
raise SwitchbotAuthenticationError(
|
|
148
|
-
f"Unexpected status code returned by SwitchBot Account API: {key_response.status_code}"
|
|
149
|
-
)
|
|
150
|
-
key_response_content = json.loads(key_response.content)
|
|
151
|
-
if key_response_content["statusCode"] != 100:
|
|
152
|
-
raise SwitchbotAuthenticationError(
|
|
153
|
-
f"Unexpected status code returned by SwitchBot API: {key_response_content['statusCode']}"
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
"key_id": key_response_content["body"]["communicationKey"]["keyId"],
|
|
158
|
-
"encryption_key": key_response_content["body"]["communicationKey"]["key"],
|
|
159
|
-
}
|
|
160
182
|
|
|
161
183
|
async def lock(self) -> bool:
|
|
162
184
|
"""Send lock command."""
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Those values have been obtained from the following files in SwitchBot Android app
|
|
2
|
-
# That's how you can verify them yourself
|
|
3
|
-
# /assets/switchbot_config.json
|
|
4
|
-
# /res/raw/amplifyconfiguration.json
|
|
5
|
-
# /res/raw/awsconfiguration.json
|
|
6
|
-
|
|
7
|
-
SWITCHBOT_APP_API_BASE_URL = "https://l9ren7efdj.execute-api.us-east-1.amazonaws.com"
|
|
8
|
-
SWITCHBOT_APP_COGNITO_POOL = {
|
|
9
|
-
"PoolId": "us-east-1_x1fixo5LC",
|
|
10
|
-
"AppClientId": "66r90hdllaj4nnlne4qna0muls",
|
|
11
|
-
"AppClientSecret": "1v3v7vfjsiggiupkeuqvsovg084e3msbefpj9rgh611u30uug6t8",
|
|
12
|
-
"Region": "us-east-1",
|
|
13
|
-
}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|