wyzeapy 0.5.28__py3-none-any.whl → 0.5.30__py3-none-any.whl
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.
- wyzeapy/__init__.py +277 -45
- wyzeapy/const.py +9 -4
- wyzeapy/crypto.py +31 -2
- wyzeapy/exceptions.py +11 -8
- wyzeapy/payload_factory.py +205 -170
- wyzeapy/services/__init__.py +3 -0
- wyzeapy/services/base_service.py +406 -212
- wyzeapy/services/bulb_service.py +67 -63
- wyzeapy/services/camera_service.py +136 -50
- wyzeapy/services/hms_service.py +8 -17
- wyzeapy/services/irrigation_service.py +189 -0
- wyzeapy/services/lock_service.py +5 -3
- wyzeapy/services/sensor_service.py +32 -11
- wyzeapy/services/switch_service.py +6 -2
- wyzeapy/services/thermostat_service.py +29 -15
- wyzeapy/services/update_manager.py +38 -11
- wyzeapy/services/wall_switch_service.py +18 -8
- wyzeapy/tests/test_irrigation_service.py +536 -0
- wyzeapy/types.py +29 -12
- wyzeapy/utils.py +98 -17
- wyzeapy/wyze_auth_lib.py +195 -37
- wyzeapy-0.5.30.dist-info/METADATA +13 -0
- wyzeapy-0.5.30.dist-info/RECORD +24 -0
- {wyzeapy-0.5.28.dist-info → wyzeapy-0.5.30.dist-info}/WHEEL +1 -1
- wyzeapy/tests/test_bulb_service.py +0 -135
- wyzeapy/tests/test_camera_service.py +0 -180
- wyzeapy/tests/test_hms_service.py +0 -90
- wyzeapy/tests/test_lock_service.py +0 -114
- wyzeapy/tests/test_sensor_service.py +0 -159
- wyzeapy/tests/test_switch_service.py +0 -138
- wyzeapy/tests/test_thermostat_service.py +0 -136
- wyzeapy/tests/test_wall_switch_service.py +0 -161
- wyzeapy-0.5.28.dist-info/LICENSES/GPL-3.0-only.txt +0 -232
- wyzeapy-0.5.28.dist-info/METADATA +0 -16
- wyzeapy-0.5.28.dist-info/RECORD +0 -31
wyzeapy/utils.py
CHANGED
|
@@ -13,6 +13,10 @@ from Crypto.Cipher import AES
|
|
|
13
13
|
from .exceptions import ParameterError, AccessTokenError, UnknownApiError
|
|
14
14
|
from .types import ResponseCodes, PropertyIDs, Device, Event
|
|
15
15
|
|
|
16
|
+
"""
|
|
17
|
+
Utility functions for encryption, decryption, error handling, and common Wyze API tasks.
|
|
18
|
+
"""
|
|
19
|
+
|
|
16
20
|
PADDING = bytes.fromhex("05")
|
|
17
21
|
|
|
18
22
|
|
|
@@ -44,7 +48,7 @@ def wyze_encrypt(key, text):
|
|
|
44
48
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
45
49
|
enc = cipher.encrypt(raw)
|
|
46
50
|
b64_enc = base64.b64encode(enc).decode("ascii")
|
|
47
|
-
b64_enc = b64_enc.replace("/", r
|
|
51
|
+
b64_enc = b64_enc.replace("/", r"\/")
|
|
48
52
|
return b64_enc
|
|
49
53
|
|
|
50
54
|
|
|
@@ -57,7 +61,7 @@ def wyze_decrypt(key, enc):
|
|
|
57
61
|
"""
|
|
58
62
|
enc = base64.b64decode(enc)
|
|
59
63
|
|
|
60
|
-
key = key.encode(
|
|
64
|
+
key = key.encode("ascii")
|
|
61
65
|
iv = key
|
|
62
66
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
63
67
|
decrypt = cipher.decrypt(enc)
|
|
@@ -68,44 +72,79 @@ def wyze_decrypt(key, enc):
|
|
|
68
72
|
|
|
69
73
|
|
|
70
74
|
def wyze_decrypt_cbc(key: str, enc_hex_str: str) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Decrypt a hex-encoded string using Wyze's CBC decryption with MD5 based key.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
key: The secret key string.
|
|
80
|
+
enc_hex_str: The encrypted data as a hex string.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The decrypted plaintext string.
|
|
84
|
+
"""
|
|
71
85
|
key_hash = hashlib.md5(key.encode("utf-8")).digest()
|
|
72
|
-
|
|
86
|
+
|
|
73
87
|
iv = b"0123456789ABCDEF"
|
|
74
88
|
cipher = AES.new(key_hash, AES.MODE_CBC, iv)
|
|
75
|
-
|
|
89
|
+
|
|
76
90
|
encrypted_bytes = binascii.unhexlify(enc_hex_str)
|
|
77
91
|
decrypted_bytes = cipher.decrypt(encrypted_bytes)
|
|
78
|
-
|
|
92
|
+
|
|
79
93
|
# PKCS5Padding
|
|
80
94
|
padding_length = decrypted_bytes[-1]
|
|
81
95
|
return decrypted_bytes[:-padding_length].decode()
|
|
82
96
|
|
|
83
97
|
|
|
84
98
|
def create_password(password: str) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Derive the Wyze API password hash using a triple MD5 hashing scheme.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
password: The plain user password string.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The hashed password as a hex string.
|
|
107
|
+
"""
|
|
85
108
|
hex1 = hashlib.md5(password.encode()).hexdigest()
|
|
86
109
|
hex2 = hashlib.md5(hex1.encode()).hexdigest()
|
|
87
110
|
return hashlib.md5(hex2.encode()).hexdigest()
|
|
88
111
|
|
|
89
112
|
|
|
90
113
|
def check_for_errors_standard(service, response_json: Dict[str, Any]) -> None:
|
|
91
|
-
|
|
114
|
+
"""
|
|
115
|
+
Check for standard Wyze API error codes and raise exceptions as needed.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
service: The service instance triggering the call.
|
|
119
|
+
response_json: The JSON response from the API.
|
|
120
|
+
"""
|
|
121
|
+
response_code = response_json["code"]
|
|
92
122
|
if response_code != ResponseCodes.SUCCESS.value:
|
|
93
123
|
if response_code == ResponseCodes.PARAMETER_ERROR.value:
|
|
94
|
-
raise ParameterError(response_code, response_json[
|
|
124
|
+
raise ParameterError(response_code, response_json["msg"])
|
|
95
125
|
elif response_code == ResponseCodes.ACCESS_TOKEN_ERROR.value:
|
|
96
126
|
service._auth_lib.token.expired = True
|
|
97
|
-
raise AccessTokenError(
|
|
127
|
+
raise AccessTokenError(
|
|
128
|
+
response_code, "Access Token expired, attempting to refresh"
|
|
129
|
+
)
|
|
98
130
|
elif response_code == ResponseCodes.DEVICE_OFFLINE.value:
|
|
99
131
|
return
|
|
100
132
|
else:
|
|
101
|
-
raise UnknownApiError(response_code, response_json[
|
|
133
|
+
raise UnknownApiError(response_code, response_json["msg"])
|
|
102
134
|
|
|
103
135
|
|
|
104
136
|
def check_for_errors_lock(service, response_json: Dict[str, Any]) -> None:
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
"""
|
|
138
|
+
Check for lock-specific API errors and raise exceptions as needed.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
service: The lock service instance.
|
|
142
|
+
response_json: The JSON response from the lock API.
|
|
143
|
+
"""
|
|
144
|
+
if response_json["ErrNo"] != 0:
|
|
145
|
+
if response_json.get("code") == ResponseCodes.PARAMETER_ERROR.value:
|
|
107
146
|
raise ParameterError(response_json)
|
|
108
|
-
elif response_json.get(
|
|
147
|
+
elif response_json.get("code") == ResponseCodes.ACCESS_TOKEN_ERROR.value:
|
|
109
148
|
service._auth_lib.token.expired = True
|
|
110
149
|
raise AccessTokenError("Access Token expired, attempting to refresh")
|
|
111
150
|
else:
|
|
@@ -113,8 +152,15 @@ def check_for_errors_lock(service, response_json: Dict[str, Any]) -> None:
|
|
|
113
152
|
|
|
114
153
|
|
|
115
154
|
def check_for_errors_devicemgmt(service, response_json: Dict[Any, Any]) -> None:
|
|
116
|
-
|
|
117
|
-
|
|
155
|
+
"""
|
|
156
|
+
Check for device management API errors and raise exceptions as needed.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
service: The device management service instance.
|
|
160
|
+
response_json: The JSON response from the device management API.
|
|
161
|
+
"""
|
|
162
|
+
if response_json["status"] != 200:
|
|
163
|
+
if "InvalidTokenError>" in response_json["response"]["errors"][0]["message"]:
|
|
118
164
|
service._auth_lib.token.expired = True
|
|
119
165
|
raise AccessTokenError("Access Token expired, attempting to refresh")
|
|
120
166
|
else:
|
|
@@ -122,20 +168,45 @@ def check_for_errors_devicemgmt(service, response_json: Dict[Any, Any]) -> None:
|
|
|
122
168
|
|
|
123
169
|
|
|
124
170
|
def check_for_errors_iot(service, response_json: Dict[Any, Any]) -> None:
|
|
125
|
-
|
|
126
|
-
|
|
171
|
+
"""
|
|
172
|
+
Check for IoT API errors and raise exceptions as needed.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
service: The IoT service instance.
|
|
176
|
+
response_json: The JSON response from the IoT API.
|
|
177
|
+
"""
|
|
178
|
+
if response_json["code"] != 1:
|
|
179
|
+
if str(response_json["code"]) == ResponseCodes.ACCESS_TOKEN_ERROR.value:
|
|
127
180
|
service._auth_lib.token.expired = True
|
|
128
181
|
raise AccessTokenError("Access Token expired, attempting to refresh")
|
|
129
182
|
else:
|
|
130
183
|
raise UnknownApiError(response_json)
|
|
131
184
|
|
|
185
|
+
|
|
132
186
|
def check_for_errors_hms(service, response_json: Dict[Any, Any]) -> None:
|
|
133
|
-
|
|
187
|
+
"""
|
|
188
|
+
Check for home monitoring system (HMS) API errors and raise exceptions as needed.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
service: The HMS service instance.
|
|
192
|
+
response_json: The JSON response from the HMS API.
|
|
193
|
+
"""
|
|
194
|
+
if response_json["message"] is None:
|
|
134
195
|
service._auth_lib.token.expired = True
|
|
135
196
|
raise AccessTokenError("Access Token expired, attempting to refresh")
|
|
136
197
|
|
|
137
198
|
|
|
138
199
|
def return_event_for_device(device: Device, events: List[Event]) -> Optional[Event]:
|
|
200
|
+
"""
|
|
201
|
+
Retrieve the most recent event matching a given device from a list of events.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
device: The device to match against event.device_mac.
|
|
205
|
+
events: List of events to search.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The first matching Event or None if no match is found.
|
|
209
|
+
"""
|
|
139
210
|
for event in events:
|
|
140
211
|
if event.device_mac == device.mac:
|
|
141
212
|
return event
|
|
@@ -144,4 +215,14 @@ def return_event_for_device(device: Device, events: List[Event]) -> Optional[Eve
|
|
|
144
215
|
|
|
145
216
|
|
|
146
217
|
def create_pid_pair(pid_enum: PropertyIDs, value: str) -> Dict[str, str]:
|
|
218
|
+
"""
|
|
219
|
+
Create a property ID/value pair dictionary for API payloads.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
pid_enum: PropertyIDs enum member for the property.
|
|
223
|
+
value: The value to set for the property.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A dict with 'pid' and 'pvalue' keys for the Wyze API.
|
|
227
|
+
"""
|
|
147
228
|
return {"pid": pid_enum.value, "pvalue": value}
|
wyzeapy/wyze_auth_lib.py
CHANGED
|
@@ -10,7 +10,17 @@ from typing import Dict, Any, Optional
|
|
|
10
10
|
|
|
11
11
|
from aiohttp import TCPConnector, ClientSession, ContentTypeError
|
|
12
12
|
|
|
13
|
-
from .const import
|
|
13
|
+
from .const import (
|
|
14
|
+
API_KEY,
|
|
15
|
+
PHONE_ID,
|
|
16
|
+
APP_NAME,
|
|
17
|
+
APP_VERSION,
|
|
18
|
+
SC,
|
|
19
|
+
SV,
|
|
20
|
+
PHONE_SYSTEM_TYPE,
|
|
21
|
+
APP_VER,
|
|
22
|
+
APP_INFO,
|
|
23
|
+
)
|
|
14
24
|
from .exceptions import (
|
|
15
25
|
UnknownApiError,
|
|
16
26
|
TwoFactorAuthenticationEnabled,
|
|
@@ -19,10 +29,28 @@ from .exceptions import (
|
|
|
19
29
|
from .utils import create_password, check_for_errors_standard
|
|
20
30
|
|
|
21
31
|
_LOGGER = logging.getLogger(__name__)
|
|
32
|
+
"""
|
|
33
|
+
Authentication token data and timing management.
|
|
34
|
+
|
|
35
|
+
This module handles Wyze API authentication tokens, including expiration
|
|
36
|
+
tracking, automatic refresh timing, and secure request methods in WyzeAuthLib.
|
|
37
|
+
"""
|
|
22
38
|
|
|
23
39
|
|
|
24
40
|
class Token:
|
|
25
|
-
|
|
41
|
+
"""Represents Wyze API access/refresh token and expiration tracking.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
_access_token: Current access token string.
|
|
45
|
+
_refresh_token: Current refresh token string.
|
|
46
|
+
expired: Flag indicating if the token is marked expired.
|
|
47
|
+
_refresh_time: Unix timestamp when token should be refreshed.
|
|
48
|
+
|
|
49
|
+
Class Attributes:
|
|
50
|
+
REFRESH_INTERVAL: Time in seconds before token auto-refresh (23h).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Token is good for 24 hours; schedule refresh after 23 hours
|
|
26
54
|
REFRESH_INTERVAL = 82800
|
|
27
55
|
|
|
28
56
|
def __init__(self, access_token, refresh_token, refresh_time: float = None):
|
|
@@ -79,6 +107,16 @@ class WyzeAuthLib:
|
|
|
79
107
|
token: Optional[Token] = None,
|
|
80
108
|
token_callback=None,
|
|
81
109
|
):
|
|
110
|
+
"""Initialize WyzeAuthLib for authentication and token management.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
username: Wyze account email address.
|
|
114
|
+
password: Plaintext or hashed account password.
|
|
115
|
+
key_id: Third-party API key ID for Wyze credentials.
|
|
116
|
+
api_key: Third-party API key for Wyze credentials.
|
|
117
|
+
token: Existing Token instance for reuse (optional).
|
|
118
|
+
token_callback: Callback to invoke on token updates.
|
|
119
|
+
"""
|
|
82
120
|
self._username = username
|
|
83
121
|
self._password = password
|
|
84
122
|
self._key_id = key_id
|
|
@@ -100,6 +138,22 @@ class WyzeAuthLib:
|
|
|
100
138
|
token: Optional[Token] = None,
|
|
101
139
|
token_callback=None,
|
|
102
140
|
):
|
|
141
|
+
"""Factory to instantiate WyzeAuthLib with credentials or existing token.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
username: Wyze account email (optional if token provided).
|
|
145
|
+
password: Wyze account password (optional if token provided).
|
|
146
|
+
key_id: Third-party API key ID (required for login).
|
|
147
|
+
api_key: Third-party API key (required for login).
|
|
148
|
+
token: Existing Token instance (skip login flow).
|
|
149
|
+
token_callback: Callback for token refresh events.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
A configured WyzeAuthLib instance.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
AttributeError: When neither credentials nor token are provided.
|
|
156
|
+
"""
|
|
103
157
|
self = cls(
|
|
104
158
|
username=username,
|
|
105
159
|
password=password,
|
|
@@ -111,7 +165,11 @@ class WyzeAuthLib:
|
|
|
111
165
|
|
|
112
166
|
if self._username is None and self._password is None and self.token is None:
|
|
113
167
|
raise AttributeError("Must provide a username, password or token")
|
|
114
|
-
elif
|
|
168
|
+
elif (
|
|
169
|
+
self.token is None
|
|
170
|
+
and self._username is not None
|
|
171
|
+
and self._password is not None
|
|
172
|
+
):
|
|
115
173
|
assert self._username != ""
|
|
116
174
|
assert self._password != ""
|
|
117
175
|
|
|
@@ -120,6 +178,22 @@ class WyzeAuthLib:
|
|
|
120
178
|
async def get_token_with_username_password(
|
|
121
179
|
self, username, password, key_id, api_key
|
|
122
180
|
) -> Token:
|
|
181
|
+
"""Authenticate using email/password and retrieve new Token.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
username: Wyze account email.
|
|
185
|
+
password: Plaintext Wyze account password.
|
|
186
|
+
key_id: Third-party API key ID.
|
|
187
|
+
api_key: Third-party API key.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
A new Token instance with access and refresh tokens.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
TwoFactorAuthenticationEnabled: When 2FA is required.
|
|
194
|
+
AccessTokenError: On invalid credentials.
|
|
195
|
+
UnknownApiError: For other authentication errors.
|
|
196
|
+
"""
|
|
123
197
|
self._username = username
|
|
124
198
|
self._password = create_password(password)
|
|
125
199
|
self._key_id = key_id
|
|
@@ -138,42 +212,57 @@ class WyzeAuthLib:
|
|
|
138
212
|
json=login_payload,
|
|
139
213
|
)
|
|
140
214
|
|
|
141
|
-
if response_json.get(
|
|
215
|
+
if response_json.get("errorCode") is not None:
|
|
142
216
|
_LOGGER.error(f"Unable to login with response from Wyze: {response_json}")
|
|
143
217
|
if response_json["errorCode"] == 1000:
|
|
144
218
|
raise AccessTokenError
|
|
145
219
|
raise UnknownApiError(response_json)
|
|
146
220
|
|
|
147
|
-
if response_json.get(
|
|
221
|
+
if response_json.get("mfa_options") is not None:
|
|
148
222
|
# Store the TOTP verification setting in the token and raise exception
|
|
149
223
|
if "TotpVerificationCode" in response_json.get("mfa_options"):
|
|
150
224
|
self.two_factor_type = "TOTP"
|
|
151
225
|
# Store the verification_id from the response, it's needed for the 2fa payload.
|
|
152
|
-
self.verification_id = response_json["mfa_details"]["totp_apps"][0][
|
|
226
|
+
self.verification_id = response_json["mfa_details"]["totp_apps"][0][
|
|
227
|
+
"app_id"
|
|
228
|
+
]
|
|
153
229
|
raise TwoFactorAuthenticationEnabled
|
|
154
230
|
# 2fa using SMS, store sms as 2fa method in token, send the code then raise exception
|
|
155
231
|
if "PrimaryPhone" in response_json.get("mfa_options"):
|
|
156
232
|
self.two_factor_type = "SMS"
|
|
157
233
|
params = {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
234
|
+
"mfaPhoneType": "Primary",
|
|
235
|
+
"sessionId": response_json.get("sms_session_id"),
|
|
236
|
+
"userId": response_json["user_id"],
|
|
161
237
|
}
|
|
162
|
-
response_json = await self.post(
|
|
163
|
-
|
|
238
|
+
response_json = await self.post(
|
|
239
|
+
"https://auth-prod.api.wyze.com/user/login/sendSmsCode",
|
|
240
|
+
headers=headers,
|
|
241
|
+
data=params,
|
|
242
|
+
)
|
|
164
243
|
# Store the session_id from this response, it's needed for the 2fa payload.
|
|
165
|
-
self.session_id = response_json[
|
|
244
|
+
self.session_id = response_json["session_id"]
|
|
166
245
|
raise TwoFactorAuthenticationEnabled
|
|
167
246
|
|
|
168
|
-
self.token = Token(
|
|
247
|
+
self.token = Token(
|
|
248
|
+
response_json["access_token"], response_json["refresh_token"]
|
|
249
|
+
)
|
|
169
250
|
await self.token_callback(self.token)
|
|
170
251
|
return self.token
|
|
171
252
|
|
|
172
253
|
async def get_token_with_2fa(self, verification_code) -> Token:
|
|
254
|
+
"""Complete login flow using two-factor authentication code.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
verification_code: 6-digit TOTP or SMS code for 2FA.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
A new Token instance after successful 2FA verification.
|
|
261
|
+
"""
|
|
173
262
|
headers = {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
263
|
+
"Phone-Id": PHONE_ID,
|
|
264
|
+
"User-Agent": APP_INFO,
|
|
265
|
+
"X-API-Key": API_KEY,
|
|
177
266
|
}
|
|
178
267
|
# TOTP Payload
|
|
179
268
|
if self.two_factor_type == "TOTP":
|
|
@@ -182,7 +271,7 @@ class WyzeAuthLib:
|
|
|
182
271
|
"password": self._password,
|
|
183
272
|
"mfa_type": "TotpVerificationCode",
|
|
184
273
|
"verification_id": self.verification_id,
|
|
185
|
-
"verification_code": verification_code
|
|
274
|
+
"verification_code": verification_code,
|
|
186
275
|
}
|
|
187
276
|
# SMS Payload
|
|
188
277
|
else:
|
|
@@ -191,22 +280,26 @@ class WyzeAuthLib:
|
|
|
191
280
|
"password": self._password,
|
|
192
281
|
"mfa_type": "PrimaryPhone",
|
|
193
282
|
"verification_id": self.session_id,
|
|
194
|
-
"verification_code": verification_code
|
|
283
|
+
"verification_code": verification_code,
|
|
195
284
|
}
|
|
196
285
|
|
|
197
286
|
response_json = await self.post(
|
|
198
|
-
|
|
199
|
-
|
|
287
|
+
"https://auth-prod.api.wyze.com/user/login", headers=headers, json=payload
|
|
288
|
+
)
|
|
200
289
|
|
|
201
|
-
self.token = Token(
|
|
290
|
+
self.token = Token(
|
|
291
|
+
response_json["access_token"], response_json["refresh_token"]
|
|
292
|
+
)
|
|
202
293
|
await self.token_callback(self.token)
|
|
203
294
|
return self.token
|
|
204
295
|
|
|
205
296
|
@property
|
|
206
297
|
def should_refresh(self) -> bool:
|
|
298
|
+
"""Check whether the current token has reached its refresh time."""
|
|
207
299
|
return time.time() >= self.token.refresh_time
|
|
208
300
|
|
|
209
301
|
async def refresh_if_should(self):
|
|
302
|
+
"""Refresh the token proactively if expired or past refresh_time."""
|
|
210
303
|
if self.should_refresh or self.token.expired:
|
|
211
304
|
async with self.refresh_lock:
|
|
212
305
|
if self.should_refresh or self.token.expired:
|
|
@@ -214,6 +307,12 @@ class WyzeAuthLib:
|
|
|
214
307
|
await self.refresh()
|
|
215
308
|
|
|
216
309
|
async def refresh(self) -> None:
|
|
310
|
+
"""Exchange the refresh token for a new access token and update internal Token.
|
|
311
|
+
|
|
312
|
+
Raises:
|
|
313
|
+
AccessTokenError: If refresh fails due to invalid refresh token.
|
|
314
|
+
UnknownApiError: For other errors during refresh.
|
|
315
|
+
"""
|
|
217
316
|
payload = {
|
|
218
317
|
"phone_id": PHONE_ID,
|
|
219
318
|
"app_name": APP_NAME,
|
|
@@ -223,25 +322,33 @@ class WyzeAuthLib:
|
|
|
223
322
|
"phone_system_type": PHONE_SYSTEM_TYPE,
|
|
224
323
|
"app_ver": APP_VER,
|
|
225
324
|
"ts": int(time.time()),
|
|
226
|
-
"refresh_token": self.token.refresh_token
|
|
325
|
+
"refresh_token": self.token.refresh_token,
|
|
227
326
|
}
|
|
228
327
|
|
|
229
|
-
headers = {
|
|
230
|
-
"X-API-Key": API_KEY
|
|
231
|
-
}
|
|
328
|
+
headers = {"X-API-Key": API_KEY}
|
|
232
329
|
|
|
233
|
-
async with ClientSession(
|
|
234
|
-
|
|
235
|
-
|
|
330
|
+
async with ClientSession(
|
|
331
|
+
connector=TCPConnector(ttl_dns_cache=(30 * 60))
|
|
332
|
+
) as _session:
|
|
333
|
+
response = await _session.post(
|
|
334
|
+
"https://api.wyzecam.com/app/user/refresh_token",
|
|
335
|
+
headers=headers,
|
|
336
|
+
json=payload,
|
|
337
|
+
)
|
|
236
338
|
response_json = await response.json()
|
|
237
339
|
check_for_errors_standard(self, response_json)
|
|
238
340
|
|
|
239
|
-
self.token.access_token = response_json[
|
|
240
|
-
self.token.refresh_token = response_json[
|
|
341
|
+
self.token.access_token = response_json["data"]["access_token"]
|
|
342
|
+
self.token.refresh_token = response_json["data"]["refresh_token"]
|
|
241
343
|
await self.token_callback(self.token)
|
|
242
344
|
self.token.expired = False
|
|
243
345
|
|
|
244
346
|
def sanitize(self, data):
|
|
347
|
+
"""Recursively sanitize sensitive fields in dicts for safe logging.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
data: The dict to sanitize; returned sanitized copy.
|
|
351
|
+
"""
|
|
245
352
|
if data and type(data) is dict:
|
|
246
353
|
# value is unused, but it prevents us from having to split the tuple to check against SANITIZE_FIELDS
|
|
247
354
|
for key, value in data.items():
|
|
@@ -252,7 +359,20 @@ class WyzeAuthLib:
|
|
|
252
359
|
return data
|
|
253
360
|
|
|
254
361
|
async def post(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
|
|
255
|
-
|
|
362
|
+
"""Send an HTTP POST request with sanitized logging.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
url: Request URL.
|
|
366
|
+
json: Optional JSON payload.
|
|
367
|
+
headers: Optional headers.
|
|
368
|
+
data: Optional form data.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Parsed JSON response.
|
|
372
|
+
"""
|
|
373
|
+
async with ClientSession(
|
|
374
|
+
connector=TCPConnector(ttl_dns_cache=(30 * 60))
|
|
375
|
+
) as _session:
|
|
256
376
|
response = await _session.post(url, json=json, headers=headers, data=data)
|
|
257
377
|
# Relocated these below as the sanitization seems to modify the data before it goes to the post.
|
|
258
378
|
_LOGGER.debug("Request:")
|
|
@@ -267,9 +387,15 @@ class WyzeAuthLib:
|
|
|
267
387
|
except ContentTypeError:
|
|
268
388
|
_LOGGER.debug(f"Response: {response}")
|
|
269
389
|
return await response.json()
|
|
270
|
-
|
|
390
|
+
|
|
271
391
|
async def put(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
|
|
272
|
-
|
|
392
|
+
"""Send an HTTP PUT request with sanitized logging.
|
|
393
|
+
|
|
394
|
+
See `post` for parameter details.
|
|
395
|
+
"""
|
|
396
|
+
async with ClientSession(
|
|
397
|
+
connector=TCPConnector(ttl_dns_cache=(30 * 60))
|
|
398
|
+
) as _session:
|
|
273
399
|
response = await _session.put(url, json=json, headers=headers, data=data)
|
|
274
400
|
# Relocated these below as the sanitization seems to modify the data before it goes to the post.
|
|
275
401
|
_LOGGER.debug("Request:")
|
|
@@ -286,7 +412,19 @@ class WyzeAuthLib:
|
|
|
286
412
|
return await response.json()
|
|
287
413
|
|
|
288
414
|
async def get(self, url, headers=None, params=None) -> Dict[Any, Any]:
|
|
289
|
-
|
|
415
|
+
"""Send an HTTP GET request with sanitized logging.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
url: Request URL.
|
|
419
|
+
headers: Optional headers.
|
|
420
|
+
params: Optional query parameters.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Parsed JSON response.
|
|
424
|
+
"""
|
|
425
|
+
async with ClientSession(
|
|
426
|
+
connector=TCPConnector(ttl_dns_cache=(30 * 60))
|
|
427
|
+
) as _session:
|
|
290
428
|
response = await _session.get(url, params=params, headers=headers)
|
|
291
429
|
# Relocated these below as the sanitization seems to modify the data before it goes to the post.
|
|
292
430
|
_LOGGER.debug("Request:")
|
|
@@ -302,8 +440,16 @@ class WyzeAuthLib:
|
|
|
302
440
|
return await response.json()
|
|
303
441
|
|
|
304
442
|
async def patch(self, url, headers=None, params=None, json=None) -> Dict[Any, Any]:
|
|
305
|
-
|
|
306
|
-
|
|
443
|
+
"""Send an HTTP PATCH request with sanitized logging.
|
|
444
|
+
|
|
445
|
+
See `get`/`post` for parameter details.
|
|
446
|
+
"""
|
|
447
|
+
async with ClientSession(
|
|
448
|
+
connector=TCPConnector(ttl_dns_cache=(30 * 60))
|
|
449
|
+
) as _session:
|
|
450
|
+
response = await _session.patch(
|
|
451
|
+
url, headers=headers, params=params, json=json
|
|
452
|
+
)
|
|
307
453
|
# Relocated these below as the sanitization seems to modify the data before it goes to the post.
|
|
308
454
|
_LOGGER.debug("Request:")
|
|
309
455
|
_LOGGER.debug(f"url: {url}")
|
|
@@ -319,7 +465,19 @@ class WyzeAuthLib:
|
|
|
319
465
|
return await response.json()
|
|
320
466
|
|
|
321
467
|
async def delete(self, url, headers=None, json=None) -> Dict[Any, Any]:
|
|
322
|
-
|
|
468
|
+
"""Send an HTTP DELETE request with sanitized logging.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
url: Request URL.
|
|
472
|
+
headers: Optional headers.
|
|
473
|
+
json: Optional JSON payload.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Parsed JSON response.
|
|
477
|
+
"""
|
|
478
|
+
async with ClientSession(
|
|
479
|
+
connector=TCPConnector(ttl_dns_cache=(30 * 60))
|
|
480
|
+
) as _session:
|
|
323
481
|
response = await _session.delete(url, headers=headers, json=json)
|
|
324
482
|
# Relocated these below as the sanitization seems to modify the data before it goes to the post.
|
|
325
483
|
_LOGGER.debug("Request:")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wyzeapy
|
|
3
|
+
Version: 0.5.30
|
|
4
|
+
Summary: A library for interacting with Wyze devices
|
|
5
|
+
Author-email: Katie Mulliken <katie@mulliken.net>
|
|
6
|
+
License: GPL-3.0-only
|
|
7
|
+
Requires-Python: >=3.11.0
|
|
8
|
+
Requires-Dist: aiodns<4.0.0,>=3.2.0
|
|
9
|
+
Requires-Dist: aiohttp<4.0.0,>=3.11.12
|
|
10
|
+
Requires-Dist: pycryptodome<4.0.0,>=3.21.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pdoc<16.0.0,>=15.0.3; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest<9.0.0,>=7.0.0; extra == 'dev'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
wyzeapy/__init__.py,sha256=QckgsDmJxrQFPdVT4rheUqi3cCBSN-jkEWD-6hHmrqk,17248
|
|
2
|
+
wyzeapy/const.py,sha256=3PV2Uq7wDD1X0ZmJBg8GWXYGHrpJvszyr9FuvwjHyus,1249
|
|
3
|
+
wyzeapy/crypto.py,sha256=ApzPL0hrrd0D4k2jB5psNatJvUSzx1Kxui6_l4NJGO8,2057
|
|
4
|
+
wyzeapy/exceptions.py,sha256=uKVWooofK22DZ3o9kwxnlJXnhk0VMJXnp-26pNavAis,1136
|
|
5
|
+
wyzeapy/payload_factory.py,sha256=D37H0rTFakqnhmYrYjBAhFvRSquujINA7b8UyhRUjl0,20547
|
|
6
|
+
wyzeapy/types.py,sha256=LPLc86FkEy8rUoPMwG8xB73NCTVRkJ1O5w7uSfDydLQ,6785
|
|
7
|
+
wyzeapy/utils.py,sha256=EXnsZFBxgI3LsIh9Ttg4gq3Aq1VMLOxBUPCspWJX9IQ,7407
|
|
8
|
+
wyzeapy/wyze_auth_lib.py,sha256=CiWdl_UAzYxKLS8yCfjXxEI87gStJede2eS4g7KlnjE,18273
|
|
9
|
+
wyzeapy/services/__init__.py,sha256=hbdyglbWQjM4XlNqPIACOEbspdsEEm4k5VXZ1hI0gc8,77
|
|
10
|
+
wyzeapy/services/base_service.py,sha256=HUIU2uugHhY0KiGvc5ALYQn4NnsFMNZzECLJgqPHR9Q,34573
|
|
11
|
+
wyzeapy/services/bulb_service.py,sha256=DNuT9PBmFhXpaD9rcjoYZMt-TLWWEAC3o0Yyvw_itHA,7825
|
|
12
|
+
wyzeapy/services/camera_service.py,sha256=vaJIChKDMg3zWcoC_JqITDBjw4sgFdyUEkl3_w2ML8I,11170
|
|
13
|
+
wyzeapy/services/hms_service.py,sha256=lQojRASz9AlwqkRfj7W7gOKXpLHrHHVwBGMw5WJ23Nc,2450
|
|
14
|
+
wyzeapy/services/irrigation_service.py,sha256=lzQoUT2COlIMF1oTTDsvb-7H7zkra96i_Cxr4zkfYzs,7110
|
|
15
|
+
wyzeapy/services/lock_service.py,sha256=NBjlr7pL5zJqdJaH33v1i6BbLHb7TXCkoCql4hCr8J8,2234
|
|
16
|
+
wyzeapy/services/sensor_service.py,sha256=WSNz0OOLoZKru4d1ZwZ80-pdJ321HssUnwEgVfwX2zM,3578
|
|
17
|
+
wyzeapy/services/switch_service.py,sha256=2O3J8-hP3vOgGVi0cKiKG_3j71zI6rHiqQd3u7CEKcE,2244
|
|
18
|
+
wyzeapy/services/thermostat_service.py,sha256=_d-UbD65JArhwsslawvwpTmfVC4tMksY-L1Uu7HW0m4,5360
|
|
19
|
+
wyzeapy/services/update_manager.py,sha256=5pZJmnyN4rlYJwMtEY13NlPBssnRLhtlLLmXr9t990Q,6770
|
|
20
|
+
wyzeapy/services/wall_switch_service.py,sha256=cBKmnB2InHKIuoPwQ47t1rDtDplyOyGQYvnfX4fXFcc,4339
|
|
21
|
+
wyzeapy/tests/test_irrigation_service.py,sha256=6rknfL0FzkptHs4CcYBamBTMD4pHcG0RyYNe3OgYBI8,21270
|
|
22
|
+
wyzeapy-0.5.30.dist-info/METADATA,sha256=7OVSrLycTNIfh3zcmTI-38EZ0xt4NZExNKamzuTT61I,445
|
|
23
|
+
wyzeapy-0.5.30.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
24
|
+
wyzeapy-0.5.30.dist-info/RECORD,,
|