PySwitchbot 0.45.0__tar.gz → 0.46.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.45.0 → pyswitchbot-0.46.0}/PKG-INFO +2 -3
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/PySwitchbot.egg-info/PKG-INFO +2 -3
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/PySwitchbot.egg-info/requires.txt +1 -2
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/setup.py +2 -3
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/__init__.py +2 -0
- pyswitchbot-0.46.0/switchbot/api_config.py +6 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/const.py +8 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/base_cover.py +0 -1
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/lock.py +77 -60
- PySwitchbot-0.45.0/switchbot/api_config.py +0 -13
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/LICENSE +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/MANIFEST.in +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/PySwitchbot.egg-info/SOURCES.txt +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/README.md +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/setup.cfg +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parser.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/__init__.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/bot.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/bulb.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/contact.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/curtain.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/lock.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/meter.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/motion.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/adv_parsers/plug.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/__init__.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/base_light.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/blind_tilt.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/bot.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/bulb.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/ceiling_light.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/contact.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/curtain.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/device.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/humidifier.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/light_strip.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/meter.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/motion.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/devices/plug.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/discovery.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/enum.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/switchbot/models.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/tests/test_adv_parser.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/tests/test_base_cover.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/tests/test_blind_tilt.py +0 -0
- {PySwitchbot-0.45.0 → pyswitchbot-0.46.0}/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.0
|
|
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.0
|
|
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.0",
|
|
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,97 @@ 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(url, json=data, headers=headers) as result:
|
|
96
|
+
if result.status > 299:
|
|
97
|
+
raise SwitchbotApiError(
|
|
98
|
+
f"Unexpected status code returned by SwitchBot API: {result.status}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
response = await result.json()
|
|
102
|
+
if response["statusCode"] != 100:
|
|
103
|
+
raise SwitchbotApiError(
|
|
104
|
+
f"{response['message']}, status code: {response['statusCode']}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return response["body"]
|
|
108
|
+
|
|
109
|
+
# Old non-async method preserved for backwards compatibility
|
|
89
110
|
@staticmethod
|
|
90
111
|
def retrieve_encryption_key(device_mac: str, username: str, password: str):
|
|
112
|
+
async def async_fn():
|
|
113
|
+
async with aiohttp.ClientSession() as session:
|
|
114
|
+
return await SwitchbotLock.async_retrieve_encryption_key(
|
|
115
|
+
session, device_mac, username, password
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return asyncio.run(async_fn())
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
async def async_retrieve_encryption_key(
|
|
122
|
+
session: aiohttp.ClientSession, device_mac: str, username: str, password: str
|
|
123
|
+
) -> dict:
|
|
91
124
|
"""Retrieve lock key from internal SwitchBot API."""
|
|
92
125
|
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
|
-
)
|
|
126
|
+
|
|
105
127
|
try:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
"
|
|
128
|
+
auth_result = await SwitchbotLock.api_request(
|
|
129
|
+
session,
|
|
130
|
+
"account",
|
|
131
|
+
"account/api/v1/user/login",
|
|
132
|
+
{
|
|
133
|
+
"clientId": SWITCHBOT_APP_CLIENT_ID,
|
|
134
|
+
"username": username,
|
|
135
|
+
"password": password,
|
|
136
|
+
"grantType": "password",
|
|
137
|
+
"verifyCode": "",
|
|
113
138
|
},
|
|
114
139
|
)
|
|
115
|
-
|
|
116
|
-
raise SwitchbotAuthenticationError(
|
|
117
|
-
f"Failed to authenticate: {err}"
|
|
118
|
-
) from err
|
|
140
|
+
auth_headers = {"authorization": auth_result["access_token"]}
|
|
119
141
|
except Exception as err:
|
|
120
|
-
raise SwitchbotAuthenticationError(
|
|
121
|
-
f"Unexpected error during authentication: {err}"
|
|
122
|
-
) from err
|
|
142
|
+
raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err
|
|
123
143
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
try:
|
|
145
|
+
userinfo = await SwitchbotLock.api_request(
|
|
146
|
+
session, "account", "account/api/v1/user/userinfo", {}, auth_headers
|
|
147
|
+
)
|
|
148
|
+
if "botRegion" in userinfo and userinfo["botRegion"] != "":
|
|
149
|
+
region = userinfo["botRegion"]
|
|
150
|
+
else:
|
|
151
|
+
region = "us"
|
|
152
|
+
except Exception as err:
|
|
153
|
+
raise SwitchbotAccountConnectionError(
|
|
154
|
+
f"Failed to retrieve SwitchBot Account user details: {err}"
|
|
155
|
+
) from err
|
|
130
156
|
|
|
131
|
-
access_token = auth_response["AuthenticationResult"]["AccessToken"]
|
|
132
157
|
try:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
device_info = await SwitchbotLock.api_request(
|
|
159
|
+
session,
|
|
160
|
+
f"wonderlabs.{region}",
|
|
161
|
+
"wonder/keys/v1/communicate",
|
|
162
|
+
{
|
|
137
163
|
"device_mac": device_mac,
|
|
138
164
|
"keyType": "user",
|
|
139
165
|
},
|
|
140
|
-
|
|
166
|
+
auth_headers,
|
|
141
167
|
)
|
|
142
|
-
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"key_id": device_info["communicationKey"]["keyId"],
|
|
171
|
+
"encryption_key": device_info["communicationKey"]["key"],
|
|
172
|
+
}
|
|
173
|
+
except Exception as err:
|
|
143
174
|
raise SwitchbotAccountConnectionError(
|
|
144
175
|
f"Failed to retrieve encryption key from SwitchBot Account: {err}"
|
|
145
176
|
) 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
177
|
|
|
161
178
|
async def lock(self) -> bool:
|
|
162
179
|
"""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
|