wyzeapy 0.5.27__py3-none-any.whl → 0.5.29__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 +267 -45
- wyzeapy/const.py +9 -4
- wyzeapy/crypto.py +31 -2
- wyzeapy/exceptions.py +11 -8
- wyzeapy/payload_factory.py +177 -172
- wyzeapy/services/__init__.py +3 -0
- wyzeapy/services/base_service.py +348 -208
- wyzeapy/services/bulb_service.py +67 -63
- wyzeapy/services/camera_service.py +136 -50
- wyzeapy/services/hms_service.py +8 -17
- wyzeapy/services/lock_service.py +13 -2
- 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/types.py +20 -12
- wyzeapy/utils.py +110 -14
- wyzeapy/wyze_auth_lib.py +195 -37
- wyzeapy-0.5.29.dist-info/METADATA +13 -0
- wyzeapy-0.5.29.dist-info/RECORD +22 -0
- {wyzeapy-0.5.27.dist-info → wyzeapy-0.5.29.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.27.dist-info/LICENSES/GPL-3.0-only.txt +0 -232
- wyzeapy-0.5.27.dist-info/METADATA +0 -15
- wyzeapy-0.5.27.dist-info/RECORD +0 -31
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.29
|
|
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,22 @@
|
|
|
1
|
+
wyzeapy/__init__.py,sha256=PeZ7J6Q9P8CsNrtpoUU7j8_OHMzupjn9pAKV7ba4ECQ,16850
|
|
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=HbkVMUsJgFiRnoOh2ppmTAQobRMgFX8DcEuGg9AMQ2k,19735
|
|
6
|
+
wyzeapy/types.py,sha256=5VP2ltvYsGZQCovYWmEefTNopz3lHxsbeFiOUtRDYfM,6591
|
|
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=wMxWIlQyW_Kmsu_U7TvIZR0ax-k22zwZywCKOG8xcPM,31671
|
|
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/lock_service.py,sha256=NBjlr7pL5zJqdJaH33v1i6BbLHb7TXCkoCql4hCr8J8,2234
|
|
15
|
+
wyzeapy/services/sensor_service.py,sha256=WSNz0OOLoZKru4d1ZwZ80-pdJ321HssUnwEgVfwX2zM,3578
|
|
16
|
+
wyzeapy/services/switch_service.py,sha256=2O3J8-hP3vOgGVi0cKiKG_3j71zI6rHiqQd3u7CEKcE,2244
|
|
17
|
+
wyzeapy/services/thermostat_service.py,sha256=_d-UbD65JArhwsslawvwpTmfVC4tMksY-L1Uu7HW0m4,5360
|
|
18
|
+
wyzeapy/services/update_manager.py,sha256=5pZJmnyN4rlYJwMtEY13NlPBssnRLhtlLLmXr9t990Q,6770
|
|
19
|
+
wyzeapy/services/wall_switch_service.py,sha256=cBKmnB2InHKIuoPwQ47t1rDtDplyOyGQYvnfX4fXFcc,4339
|
|
20
|
+
wyzeapy-0.5.29.dist-info/METADATA,sha256=xYN-zwQtZz2SYcJ0f9HetzIj7VShIyrDZzHtPQiCCyQ,445
|
|
21
|
+
wyzeapy-0.5.29.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
22
|
+
wyzeapy-0.5.29.dist-info/RECORD,,
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from unittest.mock import AsyncMock, MagicMock
|
|
3
|
-
from wyzeapy.services.bulb_service import BulbService, Bulb
|
|
4
|
-
from wyzeapy.types import DeviceTypes, PropertyIDs
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestBulbService(unittest.IsolatedAsyncioTestCase):
|
|
8
|
-
async def asyncSetUp(self):
|
|
9
|
-
mock_auth_lib = MagicMock()
|
|
10
|
-
self.bulb_service = BulbService(auth_lib=mock_auth_lib)
|
|
11
|
-
self.bulb_service._get_property_list = AsyncMock()
|
|
12
|
-
self.bulb_service.get_updated_params = AsyncMock()
|
|
13
|
-
|
|
14
|
-
async def test_update_bulb_basic_properties(self):
|
|
15
|
-
mock_bulb = Bulb({
|
|
16
|
-
"device_type": "Light",
|
|
17
|
-
"product_model": "WLPA19",
|
|
18
|
-
"mac": "TEST123",
|
|
19
|
-
"raw_dict": {},
|
|
20
|
-
"device_params": {"ip": "192.168.1.100"},
|
|
21
|
-
"prop_map": {},
|
|
22
|
-
'product_type': DeviceTypes.MESH_LIGHT.value
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
# Mock the property list response
|
|
26
|
-
self.bulb_service._get_property_list.return_value = [
|
|
27
|
-
(PropertyIDs.BRIGHTNESS, "75"),
|
|
28
|
-
(PropertyIDs.COLOR_TEMP, "4000"),
|
|
29
|
-
(PropertyIDs.ON, "1"),
|
|
30
|
-
(PropertyIDs.AVAILABLE, "1")
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
updated_bulb = await self.bulb_service.update(mock_bulb)
|
|
34
|
-
|
|
35
|
-
self.assertEqual(updated_bulb.brightness, 75)
|
|
36
|
-
self.assertEqual(updated_bulb.color_temp, 4000)
|
|
37
|
-
self.assertTrue(updated_bulb.on)
|
|
38
|
-
self.assertTrue(updated_bulb.available)
|
|
39
|
-
|
|
40
|
-
async def test_update_bulb_lightstrip_properties(self):
|
|
41
|
-
mock_bulb = Bulb({
|
|
42
|
-
"device_type": "Light",
|
|
43
|
-
"product_model": "WLST19",
|
|
44
|
-
"mac": "TEST456",
|
|
45
|
-
"raw_dict": {},
|
|
46
|
-
"device_params": {"ip": "192.168.1.101"},
|
|
47
|
-
"prop_map": {},
|
|
48
|
-
'product_type': DeviceTypes.LIGHTSTRIP.value
|
|
49
|
-
})
|
|
50
|
-
mock_bulb.product_type = DeviceTypes.LIGHTSTRIP
|
|
51
|
-
|
|
52
|
-
# Mock the property list response with the corrected color format (no # symbol)
|
|
53
|
-
self.bulb_service._get_property_list.return_value = [
|
|
54
|
-
(PropertyIDs.COLOR, "FF0000"), # Removed the # symbol
|
|
55
|
-
(PropertyIDs.COLOR_MODE, "1"),
|
|
56
|
-
(PropertyIDs.LIGHTSTRIP_EFFECTS, "rainbow"),
|
|
57
|
-
(PropertyIDs.LIGHTSTRIP_MUSIC_MODE, "1"),
|
|
58
|
-
(PropertyIDs.ON, "1"),
|
|
59
|
-
(PropertyIDs.AVAILABLE, "1")
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
updated_bulb = await self.bulb_service.update(mock_bulb)
|
|
63
|
-
|
|
64
|
-
self.assertEqual(updated_bulb.color, "FF0000")
|
|
65
|
-
self.assertEqual(updated_bulb.color_mode, "1")
|
|
66
|
-
self.assertEqual(updated_bulb.effects, "rainbow")
|
|
67
|
-
self.assertTrue(updated_bulb.music_mode)
|
|
68
|
-
self.assertTrue(updated_bulb.on)
|
|
69
|
-
self.assertTrue(updated_bulb.available)
|
|
70
|
-
|
|
71
|
-
async def test_update_bulb_sun_match(self):
|
|
72
|
-
mock_bulb = Bulb({
|
|
73
|
-
"device_type": "Light",
|
|
74
|
-
"product_model": "WLPA19",
|
|
75
|
-
"mac": "TEST789",
|
|
76
|
-
"raw_dict": {},
|
|
77
|
-
"device_params": {"ip": "192.168.1.102"},
|
|
78
|
-
"prop_map": {},
|
|
79
|
-
'product_type': DeviceTypes.MESH_LIGHT.value
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
# Mock the property list response
|
|
83
|
-
self.bulb_service._get_property_list.return_value = [
|
|
84
|
-
(PropertyIDs.SUN_MATCH, "1"),
|
|
85
|
-
(PropertyIDs.ON, "1"),
|
|
86
|
-
(PropertyIDs.AVAILABLE, "1")
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
updated_bulb = await self.bulb_service.update(mock_bulb)
|
|
90
|
-
|
|
91
|
-
self.assertTrue(updated_bulb.sun_match)
|
|
92
|
-
self.assertTrue(updated_bulb.on)
|
|
93
|
-
self.assertTrue(updated_bulb.available)
|
|
94
|
-
|
|
95
|
-
async def test_update_bulb_invalid_color_temp(self):
|
|
96
|
-
mock_bulb = Bulb({
|
|
97
|
-
"device_type": "Light",
|
|
98
|
-
"product_model": "WLPA19",
|
|
99
|
-
"mac": "TEST101",
|
|
100
|
-
"raw_dict": {},
|
|
101
|
-
"device_params": {"ip": "192.168.1.103"},
|
|
102
|
-
"prop_map": {},
|
|
103
|
-
'product_type': DeviceTypes.MESH_LIGHT.value
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
# Mock the property list response with invalid color temp
|
|
107
|
-
self.bulb_service._get_property_list.return_value = [
|
|
108
|
-
(PropertyIDs.COLOR_TEMP, "invalid"),
|
|
109
|
-
(PropertyIDs.ON, "1")
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
updated_bulb = await self.bulb_service.update(mock_bulb)
|
|
113
|
-
|
|
114
|
-
# Should default to 2700K when invalid
|
|
115
|
-
self.assertEqual(updated_bulb.color_temp, 2700)
|
|
116
|
-
self.assertTrue(updated_bulb.on)
|
|
117
|
-
|
|
118
|
-
async def test_get_bulbs(self):
|
|
119
|
-
mock_device = MagicMock()
|
|
120
|
-
mock_device.type = DeviceTypes.LIGHT
|
|
121
|
-
mock_device.raw_dict = {
|
|
122
|
-
"device_type": "Light",
|
|
123
|
-
"product_model": "WLPA19",
|
|
124
|
-
"device_params": {"ip": "192.168.1.104"},
|
|
125
|
-
"prop_map": {},
|
|
126
|
-
'product_type': DeviceTypes.MESH_LIGHT.value
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
self.bulb_service.get_object_list = AsyncMock(return_value=[mock_device])
|
|
130
|
-
|
|
131
|
-
bulbs = await self.bulb_service.get_bulbs()
|
|
132
|
-
|
|
133
|
-
self.assertEqual(len(bulbs), 1)
|
|
134
|
-
self.assertIsInstance(bulbs[0], Bulb)
|
|
135
|
-
self.bulb_service.get_object_list.assert_awaited_once()
|