wyzeapy 0.5.23__tar.gz → 0.5.24__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.
Files changed (24) hide show
  1. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/PKG-INFO +4 -4
  2. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/pyproject.toml +4 -4
  3. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/const.py +3 -0
  4. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/exceptions.py +1 -2
  5. wyzeapy-0.5.24/src/wyzeapy/payload_factory.py +570 -0
  6. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/base_service.py +119 -4
  7. wyzeapy-0.5.24/src/wyzeapy/services/camera_service.py +172 -0
  8. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/wall_switch_service.py +0 -1
  9. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/types.py +9 -5
  10. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/utils.py +18 -8
  11. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/wyze_auth_lib.py +17 -0
  12. wyzeapy-0.5.23/src/wyzeapy/payload_factory.py +0 -74
  13. wyzeapy-0.5.23/src/wyzeapy/services/camera_service.py +0 -136
  14. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/LICENSES/GPL-3.0-only.txt +0 -0
  15. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/__init__.py +0 -0
  16. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/crypto.py +0 -0
  17. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/__init__.py +0 -0
  18. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/bulb_service.py +0 -0
  19. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/hms_service.py +0 -0
  20. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/lock_service.py +0 -0
  21. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/sensor_service.py +0 -0
  22. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/switch_service.py +0 -0
  23. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/thermostat_service.py +0 -0
  24. {wyzeapy-0.5.23 → wyzeapy-0.5.24}/src/wyzeapy/services/update_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wyzeapy
3
- Version: 0.5.23
3
+ Version: 0.5.24
4
4
  Summary: A library for interacting with Wyze devices
5
5
  License: GPL-3.0-only
6
6
  Author: Katie Mulliken
@@ -10,6 +10,6 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
- Requires-Dist: aiodns (>=3.0.0,<4.0.0)
14
- Requires-Dist: aiohttp (>=3.7,<4.0)
15
- Requires-Dist: pycryptodome (>=3.12.0,<4.0.0)
13
+ Requires-Dist: aiodns (>=3.2.0,<4.0.0)
14
+ Requires-Dist: aiohttp (>=3.10.9,<4.0.0)
15
+ Requires-Dist: pycryptodome (>=3.21.0,<4.0.0)
@@ -1,15 +1,15 @@
1
1
  [tool.poetry]
2
2
  name = "wyzeapy"
3
- version = "0.5.23"
3
+ version = "0.5.24"
4
4
  description = "A library for interacting with Wyze devices"
5
5
  authors = ["Katie Mulliken <katie@mulliken.net>"]
6
6
  license = "GPL-3.0-only"
7
7
 
8
8
  [tool.poetry.dependencies]
9
9
  python = ">=3.11.0"
10
- aiohttp = "^3.7"
11
- aiodns = "^3.0.0"
12
- pycryptodome = "^3.12.0"
10
+ aiohttp = "^3.10.9"
11
+ aiodns = "^3.2.0"
12
+ pycryptodome = "^3.21.0"
13
13
 
14
14
  [tool.poetry.dev-dependencies]
15
15
 
@@ -15,6 +15,9 @@ PHONE_ID = str(uuid.uuid4())
15
15
  APP_INFO = 'wyze_android_2.19.14' # Required for the thermostat
16
16
  SC = "9f275790cab94a72bd206c8876429f3c"
17
17
  SV = "9d74946e652647e9b6c9d59326aef104"
18
+ CLIENT_VER = "2"
19
+ SOURCE = "ios/WZCameraSDK"
20
+ APP_PLATFORM = "ios"
18
21
 
19
22
  # Crypto secrets
20
23
  OLIVE_SIGNING_SECRET = 'wyze_app_secret_key_132' # Required for the thermostat
@@ -26,8 +26,7 @@ class LoginError(Exception):
26
26
 
27
27
 
28
28
  class UnknownApiError(Exception):
29
- def __init__(self, response_json: Dict[str, Any]):
30
- super(UnknownApiError, self).__init__(str(response_json))
29
+ pass
31
30
 
32
31
 
33
32
  class TwoFactorAuthenticationEnabled(Exception):
@@ -0,0 +1,570 @@
1
+ # Copyright (c) 2021. Mulliken, LLC - All Rights Reserved
2
+ # You may use, distribute and modify this code under the terms
3
+ # of the attached license. You should have received a copy of
4
+ # the license with this file. If not, please write to:
5
+ # katie@mulliken.net to receive a copy
6
+ import time
7
+ from typing import Any, Dict
8
+
9
+ from .const import FORD_APP_KEY
10
+ from .crypto import ford_create_signature
11
+
12
+
13
+ def ford_create_payload(access_token: str, payload: Dict[str, Any],
14
+ url_path: str, request_method: str) -> Dict[str, Any]:
15
+ payload["access_token"] = access_token
16
+ payload["key"] = FORD_APP_KEY
17
+ payload["timestamp"] = str(int(time.time() * 1000))
18
+ payload["sign"] = ford_create_signature(url_path, request_method, payload)
19
+ return payload
20
+
21
+
22
+ def olive_create_get_payload(device_mac: str, keys: str) -> Dict[str, Any]:
23
+ nonce = int(time.time() * 1000)
24
+
25
+ return {
26
+ 'keys': keys,
27
+ 'did': device_mac,
28
+ 'nonce': nonce
29
+ }
30
+
31
+
32
+ def olive_create_post_payload(device_mac: str, device_model: str, prop_key: str, value: Any) -> Dict[str, Any]:
33
+ nonce = int(time.time() * 1000)
34
+
35
+ return {
36
+ "did": device_mac,
37
+ "model": device_model,
38
+ "props": {
39
+ prop_key: value
40
+ },
41
+ "is_sub_device": 0,
42
+ "nonce": str(nonce)
43
+ }
44
+
45
+
46
+ def olive_create_hms_payload() -> Dict[str, str]:
47
+ nonce = int(time.time() * 1000)
48
+
49
+ return {
50
+ "group_id": "hms",
51
+ "nonce": str(nonce)
52
+ }
53
+
54
+
55
+ def olive_create_user_info_payload() -> Dict[str, str]:
56
+ nonce = int(time.time() * 1000)
57
+
58
+ return {
59
+ "nonce": str(nonce)
60
+ }
61
+
62
+
63
+ def olive_create_hms_get_payload(hms_id: str) -> Dict[str, str]:
64
+ nonce = int(time.time() * 1000)
65
+ return {
66
+ "hms_id": hms_id,
67
+ "nonce": str(nonce)
68
+ }
69
+
70
+
71
+ def olive_create_hms_patch_payload(hms_id: str) -> Dict[str, Any]:
72
+ return {
73
+ "hms_id": hms_id
74
+ }
75
+
76
+
77
+ def devicemgmt_create_capabilities_payload(type: str, value: str):
78
+ match type:
79
+ case "floodlight":
80
+ return {
81
+ "iid": 4,
82
+ "name": "floodlight",
83
+ "properties": [
84
+ {
85
+ "prop": "on",
86
+ "value": value
87
+ }
88
+ ]
89
+ }
90
+ case "spotlight":
91
+ return {
92
+ "iid": 5,
93
+ "name": "spotlight",
94
+ "properties": [
95
+ {
96
+ "prop": "on",
97
+ "value": value
98
+ }
99
+ ]
100
+ }
101
+ case "power":
102
+ return {
103
+ "functions": [
104
+ {
105
+ "in": {
106
+ "wakeup-live-view": "1"
107
+ },
108
+ "name": value
109
+ }
110
+ ],
111
+ "iid": 1,
112
+ "name": "iot-device"
113
+ }
114
+ case "siren":
115
+ return {
116
+ "functions": [
117
+ {
118
+ "in": {},
119
+ "name": value
120
+ }
121
+ ],
122
+ "name": "siren"
123
+ }
124
+ case _:
125
+ raise NotImplementedError(f"No action of type ({type}) has been implemented.")
126
+
127
+
128
+ def devicemgmt_get_iot_props_list(model: str):
129
+ match model:
130
+ case "LD_CFP": # Floodlight Pro
131
+ return [
132
+ {
133
+ "iid": 2,
134
+ "name": "camera",
135
+ "properties": [
136
+ "motion-detect",
137
+ "resolution",
138
+ "bit-rate",
139
+ "live-stream-mode",
140
+ "recording-mode",
141
+ "frame-rate",
142
+ "night-shot",
143
+ "night-shot-state",
144
+ "rotate-angle",
145
+ "time-watermark",
146
+ "logo-watermark",
147
+ "recording-trigger-source",
148
+ "recording-content-type",
149
+ "motion-push",
150
+ "speaker",
151
+ "microphone",
152
+ "unusual-sound-push",
153
+ "flip",
154
+ "motion-detect-recording",
155
+ "cool-down-interval",
156
+ "infrared-mode",
157
+ "sound-collection-on",
158
+ "live-stream-protocol",
159
+ "ai-push",
160
+ "voice-template",
161
+ "motion-category"
162
+ ]
163
+ },
164
+ {
165
+ "iid": 3,
166
+ "name": "device-info",
167
+ "properties": [
168
+ "device-id",
169
+ "device-model",
170
+ "firmware-ver",
171
+ "mac",
172
+ "timezone",
173
+ "lat",
174
+ "ip",
175
+ "lon",
176
+ "hardware-ver",
177
+ "public-ip"
178
+ ]
179
+ },
180
+ {
181
+ "iid": 1,
182
+ "name": "iot-device",
183
+ "properties": [
184
+ "iot-state",
185
+ "iot-power",
186
+ "push-switch"
187
+ ]
188
+ },
189
+ {
190
+ "iid": 9,
191
+ "name": "camera-ai",
192
+ "properties": [
193
+ "smart-detection-type",
194
+ "on"
195
+ ]
196
+ },
197
+ {
198
+ "iid": 4,
199
+ "name": "floodlight",
200
+ "properties": [
201
+ "on",
202
+ "enabled",
203
+ "mode",
204
+ "trigger-source",
205
+ "brightness",
206
+ "light-on-duration",
207
+ "voice-template",
208
+ "motion-warning-switch",
209
+ "motion-activate-light-switch",
210
+ "motion-activate-light-schedule",
211
+ "motion-activate-brightness",
212
+ "ambient-light-switch",
213
+ "ambient-light-schedule",
214
+ "ambient-light-brightness",
215
+ "motion-tag",
216
+ "light-model",
217
+ "flash-with-siren"
218
+ ]
219
+ },
220
+ {
221
+ "iid": 11,
222
+ "name": "indicator-light",
223
+ "properties": [
224
+ "on",
225
+ "mode",
226
+ "brightness",
227
+ "color",
228
+ "color-temperature"
229
+ ]
230
+ },
231
+ {
232
+ "iid": 8,
233
+ "name": "memory-card-management",
234
+ "properties": [
235
+ "storage-used-space",
236
+ "storage-total-space",
237
+ "storage-status",
238
+ "sd-card-playback-enabled"
239
+ ]
240
+ },
241
+ {
242
+ "iid": 6,
243
+ "name": "motion-detection",
244
+ "properties": [
245
+ "sensitivity-motion",
246
+ "on",
247
+ "motion-zone",
248
+ "motion-zone-selected-block",
249
+ "motion-zone-block-size",
250
+ "motion-tag",
251
+ "edge-detection-type",
252
+ "motion-warning-switch",
253
+ "motion-warning-tone",
254
+ "motion-warning-interval",
255
+ "motion-warning-schedule",
256
+ "motion-warning-sound",
257
+ "motion-warning-trigger-setting"
258
+ ]
259
+ },
260
+ {
261
+ "iid": 7,
262
+ "name": "siren",
263
+ "properties": [
264
+ "state"
265
+ ]
266
+ },
267
+ {
268
+ "iid": 5,
269
+ "name": "wifi",
270
+ "properties": [
271
+ "on",
272
+ "signal-strength",
273
+ "wifi-ssid",
274
+ "wifi-encrypted-password"
275
+ ]
276
+ }
277
+ ]
278
+ case "AN_RSCW": # Battery Cam pro
279
+ return [
280
+ {
281
+ "iid": 2,
282
+ "name": "camera",
283
+ "properties": [
284
+ "motion-detect",
285
+ "resolution",
286
+ "bit-rate",
287
+ "live-stream-mode",
288
+ "recording-mode",
289
+ "frame-rate",
290
+ "night-shot",
291
+ "night-shot-state",
292
+ "time-watermark",
293
+ "logo-watermark",
294
+ "cool-down-interval",
295
+ "recording-content-type",
296
+ "video-length-limit",
297
+ "motion-push",
298
+ "speaker",
299
+ "unusual-sound-push",
300
+ "microphone",
301
+ "infrared-mode",
302
+ "motion-detect-recording",
303
+ "live-stream-protocol",
304
+ "recording-resolution",
305
+ "recording-start-time",
306
+ "recording-schedule-duration",
307
+ "voice-template",
308
+ "rotate-angle",
309
+ "sound-collection-on",
310
+ "ai-push"
311
+ ]
312
+ },
313
+ {
314
+ "iid": 3,
315
+ "name": "device-info",
316
+ "properties": [
317
+ "device-id",
318
+ "device-model",
319
+ "firmware-ver",
320
+ "mac",
321
+ "timezone",
322
+ "lat",
323
+ "ip",
324
+ "lon",
325
+ "company-code",
326
+ "device-setting-channel",
327
+ "network-connection-mode",
328
+ "hardware-ver",
329
+ "public-ip"
330
+ ]
331
+ },
332
+ {
333
+ "iid": 1,
334
+ "name": "iot-device",
335
+ "properties": [
336
+ "iot-state",
337
+ "iot-power",
338
+ "push-switch",
339
+ "mqtt-check"
340
+ ]
341
+ },
342
+ {
343
+ "iid": 7,
344
+ "name": "battery",
345
+ "properties": [
346
+ "battery-level",
347
+ "low-battery-push",
348
+ "power-source",
349
+ "charging-status",
350
+ "power-saving"
351
+ ]
352
+ },
353
+ {
354
+ "iid": 12,
355
+ "name": "camera-ai",
356
+ "properties": [
357
+ "smart-detection-type",
358
+ "on"
359
+ ]
360
+ },
361
+ {
362
+ "iid": 8,
363
+ "name": "indicator-light",
364
+ "properties": [
365
+ "on",
366
+ "mode"
367
+ ]
368
+ },
369
+ {
370
+ "iid": 6,
371
+ "name": "memory-card-management",
372
+ "properties": [
373
+ "storage-used-space",
374
+ "storage-total-space",
375
+ "storage-status",
376
+ "sd-card-playback-enabled"
377
+ ]
378
+ },
379
+ {
380
+ "iid": 11,
381
+ "name": "motion-detection",
382
+ "properties": [
383
+ "sensitivity-motion",
384
+ "on",
385
+ "area-length",
386
+ "motion-zone",
387
+ "motion-zone-block-size",
388
+ "motion-zone-selected-block",
389
+ "edge-detection-type",
390
+ "motion-tag"
391
+ ]
392
+ },
393
+ {
394
+ "iid": 4,
395
+ "name": "siren",
396
+ "properties": [
397
+ "state",
398
+ "siren-on-ts"
399
+ ]
400
+ },
401
+ {
402
+ "iid": 14,
403
+ "name": "solar-panel",
404
+ "properties": [
405
+ "enabled",
406
+ "on"
407
+ ]
408
+ },
409
+ {
410
+ "iid": 5,
411
+ "name": "spotlight",
412
+ "properties": [
413
+ "on",
414
+ "enabled",
415
+ "brightness",
416
+ "motion-activate-light-switch",
417
+ "sunset-to-sunrise",
418
+ "motion-activate-light-schedule",
419
+ "trigger-source"
420
+ ]
421
+ },
422
+ {
423
+ "iid": 9,
424
+ "name": "wifi",
425
+ "properties": [
426
+ "on",
427
+ "signal-strength",
428
+ "wifi-ssid",
429
+ "wifi-encrypted-password"
430
+ ]
431
+ }
432
+ ]
433
+ case "GW_GC1": # OG
434
+ return [
435
+ {
436
+ "iid": 2,
437
+ "name": "camera",
438
+ "properties": [
439
+ "motion-detect",
440
+ "resolution",
441
+ "bit-rate",
442
+ "live-stream-mode",
443
+ "recording-mode",
444
+ "frame-rate",
445
+ "night-shot",
446
+ "night-shot-state",
447
+ "time-watermark",
448
+ "logo-watermark",
449
+ "cool-down-interval",
450
+ "recording-content-type",
451
+ "video-length-limit",
452
+ "motion-push",
453
+ "speaker",
454
+ "unusual-sound-push",
455
+ "microphone",
456
+ "infrared-mode",
457
+ "motion-detect-recording",
458
+ "live-stream-protocol",
459
+ "recording-resolution",
460
+ "recording-start-time",
461
+ "recording-schedule-duration",
462
+ "voice-template",
463
+ "rotate-angle",
464
+ "sound-collection-on",
465
+ "ai-push"
466
+ ]
467
+ },
468
+ {
469
+ "iid": 3,
470
+ "name": "device-info",
471
+ "properties": [
472
+ "device-id",
473
+ "device-model",
474
+ "firmware-ver",
475
+ "mac",
476
+ "timezone",
477
+ "lat",
478
+ "ip",
479
+ "lon",
480
+ "company-code",
481
+ "device-setting-channel",
482
+ "network-connection-mode",
483
+ "hardware-ver",
484
+ "public-ip"
485
+ ]
486
+ },
487
+ {
488
+ "iid": 1,
489
+ "name": "iot-device",
490
+ "properties": [
491
+ "iot-state",
492
+ "iot-power",
493
+ "push-switch",
494
+ "mqtt-check"
495
+ ]
496
+ },
497
+ {
498
+ "iid": 12,
499
+ "name": "camera-ai",
500
+ "properties": [
501
+ "smart-detection-type",
502
+ "on"
503
+ ]
504
+ },
505
+ {
506
+ "iid": 8,
507
+ "name": "indicator-light",
508
+ "properties": [
509
+ "on",
510
+ "mode"
511
+ ]
512
+ },
513
+ {
514
+ "iid": 6,
515
+ "name": "memory-card-management",
516
+ "properties": [
517
+ "storage-used-space",
518
+ "storage-total-space",
519
+ "storage-status",
520
+ "sd-card-playback-enabled"
521
+ ]
522
+ },
523
+ {
524
+ "iid": 11,
525
+ "name": "motion-detection",
526
+ "properties": [
527
+ "sensitivity-motion",
528
+ "on",
529
+ "area-length",
530
+ "motion-zone",
531
+ "motion-zone-block-size",
532
+ "motion-zone-selected-block",
533
+ "edge-detection-type",
534
+ "motion-tag"
535
+ ]
536
+ },
537
+ {
538
+ "iid": 4,
539
+ "name": "siren",
540
+ "properties": [
541
+ "state",
542
+ "siren-on-ts"
543
+ ]
544
+ },
545
+ {
546
+ "iid": 5,
547
+ "name": "spotlight",
548
+ "properties": [
549
+ "on",
550
+ "enabled",
551
+ "brightness",
552
+ "motion-activate-light-switch",
553
+ "sunset-to-sunrise",
554
+ "motion-activate-light-schedule",
555
+ "trigger-source"
556
+ ]
557
+ },
558
+ {
559
+ "iid": 9,
560
+ "name": "wifi",
561
+ "properties": [
562
+ "on",
563
+ "signal-strength",
564
+ "wifi-ssid",
565
+ "wifi-encrypted-password"
566
+ ]
567
+ }
568
+ ]
569
+ case _:
570
+ raise NotImplementedError(f"No iot props for model ({model}) have been defined.")
@@ -12,14 +12,14 @@ from typing import List, Tuple, Any, Dict, Optional
12
12
  import aiohttp
13
13
 
14
14
  from .update_manager import DeviceUpdater, UpdateManager
15
- from ..const import PHONE_SYSTEM_TYPE, APP_VERSION, APP_VER, PHONE_ID, APP_NAME, OLIVE_APP_ID, APP_INFO, SC, SV
15
+ from ..const import PHONE_SYSTEM_TYPE, APP_VERSION, APP_VER, PHONE_ID, APP_NAME, OLIVE_APP_ID, APP_INFO, SC, SV, APP_PLATFORM, SOURCE
16
16
  from ..crypto import olive_create_signature
17
17
  from ..payload_factory import olive_create_hms_patch_payload, olive_create_hms_payload, \
18
18
  olive_create_hms_get_payload, ford_create_payload, olive_create_get_payload, olive_create_post_payload, \
19
- olive_create_user_info_payload
20
- from ..types import PropertyIDs, Device
19
+ olive_create_user_info_payload, devicemgmt_create_capabilities_payload, devicemgmt_get_iot_props_list
20
+ from ..types import PropertyIDs, Device, DeviceMgmtToggleType
21
21
  from ..utils import check_for_errors_standard, check_for_errors_hms, check_for_errors_lock, \
22
- check_for_errors_iot, wyze_encrypt
22
+ check_for_errors_iot, wyze_encrypt, check_for_errors_devicemgmt
23
23
  from ..wyze_auth_lib import WyzeAuthLib
24
24
 
25
25
  _LOGGER = logging.getLogger(__name__)
@@ -326,6 +326,121 @@ class BaseService:
326
326
  json=payload)
327
327
 
328
328
  check_for_errors_standard(self, response_json)
329
+
330
+ async def _run_action_devicemgmt(self, device: Device, type: str, value: str) -> None:
331
+ """
332
+ Wraps the devicemgmt-service-beta.wyze.com/device-management/api/action/run_action endpoint
333
+
334
+ :param device: The device for which to run the action
335
+ :param state: on or off
336
+ :return:
337
+ """
338
+
339
+ await self._auth_lib.refresh_if_should()
340
+
341
+ capabilities = devicemgmt_create_capabilities_payload(type, value)
342
+
343
+ payload = {
344
+ "capabilities": [
345
+ capabilities
346
+ ],
347
+ "nonce": int(time.time() * 1000),
348
+ "targetInfo": {
349
+ "id": device.mac,
350
+ "productModel": device.product_model,
351
+ "type": "DEVICE"
352
+ },
353
+ "transactionId": "0a5b20591fedd4du1b93f90743ba0csd" # OG cam needs this (doesn't matter what the value is)
354
+ }
355
+
356
+ headers = {
357
+ "authorization": self._auth_lib.token.access_token,
358
+ }
359
+
360
+ response_json = await self._auth_lib.post("https://devicemgmt-service-beta.wyze.com/device-management/api/action/run_action",
361
+ json=payload, headers=headers)
362
+
363
+ check_for_errors_iot(self, response_json)
364
+
365
+ async def _set_toggle(self, device: Device, toggleType: DeviceMgmtToggleType, state: str) -> None:
366
+ """
367
+ Wraps the ai-subscription-service-beta.wyzecam.com/v4/subscription-service/toggle-management endpoint
368
+
369
+ :param device: The device for which to get the state
370
+ :param toggleType: Enum for the toggle type
371
+ :param state: String state to set for the toggle
372
+ """
373
+
374
+ await self._auth_lib.refresh_if_should()
375
+
376
+ payload = {
377
+ "data": [
378
+ {
379
+ "device_firmware": "1234567890",
380
+ "device_id": device.mac,
381
+ "device_model": device.product_model,
382
+ "page_id": [
383
+ toggleType.pageId
384
+ ],
385
+ "toggle_update": [
386
+ {
387
+ "toggle_id": toggleType.toggleId,
388
+ "toggle_status": state
389
+ }
390
+ ]
391
+ }
392
+ ],
393
+ "nonce": str(int(time.time() * 1000))
394
+ }
395
+
396
+
397
+ signature = olive_create_signature(payload, self._auth_lib.token.access_token)
398
+ headers = {
399
+ "access_token": self._auth_lib.token.access_token,
400
+ "timestamp": str(int(time.time() * 1000)),
401
+ "appid": OLIVE_APP_ID,
402
+ "source": SOURCE,
403
+ "signature2": signature,
404
+ "appplatform": APP_PLATFORM,
405
+ "appversion": APP_VERSION,
406
+ "requestid": "35374158s4s313b9a2be7c057f2da5d1"
407
+ }
408
+
409
+ response_json = await self._auth_lib.put("https://ai-subscription-service-beta.wyzecam.com/v4/subscription-service/toggle-management",
410
+ json=payload, headers=headers)
411
+
412
+ check_for_errors_devicemgmt(self, response_json)
413
+
414
+ async def _get_iot_prop_devicemgmt(self, device: Device) -> Dict[str, Any]:
415
+ """
416
+ Wraps the devicemgmt-service-beta.wyze.com/device-management/api/device-property/get_iot_prop endpoint
417
+
418
+ :param device: The device for which to get the state
419
+ :return:
420
+ """
421
+
422
+ await self._auth_lib.refresh_if_should()
423
+
424
+ payload = {
425
+ "capabilities": devicemgmt_get_iot_props_list(device.product_model),
426
+ "nonce": int(time.time() * 1000),
427
+ "targetInfo": {
428
+ "id": device.mac,
429
+ "productModel": device.product_model,
430
+ "type": "DEVICE"
431
+ }
432
+ }
433
+
434
+ headers = {
435
+ "authorization": self._auth_lib.token.access_token,
436
+ }
437
+
438
+ response_json = await self._auth_lib.post("https://devicemgmt-service-beta.wyze.com/device-management/api/device-property/get_iot_prop",
439
+ json=payload, headers=headers)
440
+
441
+ check_for_errors_iot(self, response_json)
442
+
443
+ return response_json
329
444
 
330
445
  async def _set_property(self, device: Device, pid: str, pvalue: str) -> None:
331
446
  """
@@ -0,0 +1,172 @@
1
+ # Copyright (c) 2021. Mulliken, LLC - All Rights Reserved
2
+ # You may use, distribute and modify this code under the terms
3
+ # of the attached license. You should have received a copy of
4
+ # the license with this file. If not, please write to:
5
+ # katie@mulliken.net to receive a copy
6
+ import asyncio
7
+ import logging
8
+ import time
9
+ from threading import Thread
10
+ from typing import Any, List, Optional, Dict, Callable, Tuple
11
+
12
+ from aiohttp import ClientOSError, ContentTypeError
13
+
14
+ from ..exceptions import UnknownApiError
15
+ from .base_service import BaseService
16
+ from .update_manager import DeviceUpdater
17
+ from ..types import Device, DeviceTypes, Event, PropertyIDs, DeviceMgmtToggleProps
18
+ from ..utils import return_event_for_device, create_pid_pair
19
+
20
+ _LOGGER = logging.getLogger(__name__)
21
+
22
+ # NOTE: Make sure to also define props in devicemgmt_create_capabilities_payload()
23
+ DEVICEMGMT_API_MODELS = ["LD_CFP", "AN_RSCW", "GW_GC1"] # Floodlight pro, battery cam pro, and OG use a diffrent api (devicemgmt)
24
+
25
+
26
+ class Camera(Device):
27
+ def __init__(self, dictionary: Dict[Any, Any]):
28
+ super().__init__(dictionary)
29
+
30
+ self.last_event: Optional[Event] = None
31
+ self.last_event_ts: int = int(time.time() * 1000)
32
+ self.on: bool = True
33
+ self.siren: bool = False
34
+ self.floodlight: bool = False
35
+
36
+
37
+ class CameraService(BaseService):
38
+ _updater_thread: Optional[Thread] = None
39
+ _subscribers: List[Tuple[Camera, Callable[[Camera], None]]] = []
40
+
41
+ async def update(self, camera: Camera):
42
+ # Get updated device_params
43
+ async with BaseService._update_lock:
44
+ camera.device_params = await self.get_updated_params(camera.mac)
45
+
46
+ # Get camera events
47
+ response = await self._get_event_list(10)
48
+ raw_events = response['data']['event_list']
49
+ latest_events = [Event(raw_event) for raw_event in raw_events]
50
+
51
+ if (event := return_event_for_device(camera, latest_events)) is not None:
52
+ camera.last_event = event
53
+ camera.last_event_ts = event.event_ts
54
+
55
+ # Update camera state
56
+ if (camera.product_model in DEVICEMGMT_API_MODELS): # New api
57
+ state_response: Dict[str, Any] = await self._get_iot_prop_devicemgmt(camera)
58
+ for propCategory in state_response['data']['capabilities']:
59
+ if propCategory['name'] == "camera":
60
+ camera.motion = propCategory['properties']['motion-detect-recording']
61
+ if propCategory['name'] == "floodlight" or propCategory['name'] == "spotlight":
62
+ camera.floodlight = propCategory['properties']['on']
63
+ if propCategory['name'] == "siren":
64
+ camera.siren = propCategory['properties']['state']
65
+ if propCategory['name'] == "iot-device":
66
+ camera.notify = propCategory['properties']['push-switch']
67
+ camera.on = propCategory['properties']['iot-power']
68
+ camera.available = propCategory['properties']['iot-state']
69
+
70
+ else: # All other cam types (old api?)
71
+ state_response: List[Tuple[PropertyIDs, Any]] = await self._get_property_list(camera)
72
+ for property, value in state_response:
73
+ if property is PropertyIDs.AVAILABLE:
74
+ camera.available = value == "1"
75
+ if property is PropertyIDs.ON:
76
+ camera.on = value == "1"
77
+ if property is PropertyIDs.CAMERA_SIREN:
78
+ camera.siren = value == "1"
79
+ if property is PropertyIDs.FLOOD_LIGHT:
80
+ camera.floodlight = value == "1"
81
+ if property is PropertyIDs.NOTIFICATION:
82
+ camera.notify = value == "1"
83
+ if property is PropertyIDs.MOTION_DETECTION:
84
+ camera.motion = value == "1"
85
+
86
+ return camera
87
+
88
+ async def register_for_updates(self, camera: Camera, callback: Callable[[Camera], None]):
89
+ loop = asyncio.get_event_loop()
90
+ if not self._updater_thread:
91
+ self._updater_thread = Thread(target=self.update_worker, args=[loop, ], daemon=True)
92
+ self._updater_thread.start()
93
+
94
+ self._subscribers.append((camera, callback))
95
+
96
+ async def deregister_for_updates(self, camera: Camera):
97
+ self._subscribers = [(cam, callback) for cam, callback in self._subscribers if cam.mac != camera.mac]
98
+
99
+ def update_worker(self, loop):
100
+ while True:
101
+ if len(self._subscribers) < 1:
102
+ time.sleep(0.1)
103
+ else:
104
+ for camera, callback in self._subscribers:
105
+ try:
106
+ callback(asyncio.run_coroutine_threadsafe(self.update(camera), loop).result())
107
+ except UnknownApiError as e:
108
+ _LOGGER.warning(f"The update method detected an UnknownApiError: {e}")
109
+ except ClientOSError as e:
110
+ _LOGGER.error(f"A network error was detected: {e}")
111
+ except ContentTypeError as e:
112
+ _LOGGER.error(f"Server returned unexpected ContentType: {e}")
113
+
114
+ async def get_cameras(self) -> List[Camera]:
115
+ if self._devices is None:
116
+ self._devices = await self.get_object_list()
117
+
118
+ cameras = [device for device in self._devices if device.type is DeviceTypes.CAMERA]
119
+
120
+ return [Camera(camera.raw_dict) for camera in cameras]
121
+
122
+ async def turn_on(self, camera: Camera):
123
+ if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "power", "wakeup") # Some camera models use a diffrent api
124
+ else: await self._run_action(camera, "power_on")
125
+
126
+ async def turn_off(self, camera: Camera):
127
+ if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "power", "sleep") # Some camera models use a diffrent api
128
+ else: await self._run_action(camera, "power_off")
129
+
130
+ async def siren_on(self, camera: Camera):
131
+ if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "siren", "siren-on") # Some camera models use a diffrent api
132
+ else: await self._run_action(camera, "siren_on")
133
+
134
+ async def siren_off(self, camera: Camera):
135
+ if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "siren", "siren-off") # Some camera models use a diffrent api
136
+ else: await self._run_action(camera, "siren_off")
137
+
138
+ # Also controls lamp socket and BCP spotlight
139
+ async def floodlight_on(self, camera: Camera):
140
+ if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "1") # Battery cam pro integrated spotlight is controllable
141
+ elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "1") # Some camera models use a diffrent api
142
+ else: await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "1")
143
+
144
+ # Also controls lamp socket and BCP spotlight
145
+ async def floodlight_off(self, camera: Camera):
146
+ if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "0") # Battery cam pro integrated spotlight is controllable
147
+ elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "0") # Some camera models use a diffrent api
148
+ else: await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "2")
149
+
150
+ async def turn_on_notifications(self, camera: Camera):
151
+ if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.NOTIFICATION_TOGGLE.value, "1")
152
+ else: await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "1")
153
+
154
+ async def turn_off_notifications(self, camera: Camera):
155
+ if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.NOTIFICATION_TOGGLE.value, "0")
156
+ else: await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "0")
157
+
158
+ # Both properties need to be set on newer cams, older cameras seem to only react
159
+ # to the first property but it doesnt hurt to set both
160
+ async def turn_on_motion_detection(self, camera: Camera):
161
+ if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.EVENT_RECORDING_TOGGLE.value, "1")
162
+ elif (camera.product_model in ["WVOD1", "HL_WCO2"]): await self._set_property_list(camera, [create_pid_pair(PropertyIDs.WCO_MOTION_DETECTION, "1")])
163
+ else:
164
+ await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "1")
165
+ await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "1")
166
+
167
+ async def turn_off_motion_detection(self, camera: Camera):
168
+ if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.EVENT_RECORDING_TOGGLE.value, "0")
169
+ elif (camera.product_model in ["WVOD1", "HL_WCO2"]): await self._set_property_list(camera, [create_pid_pair(PropertyIDs.WCO_MOTION_DETECTION, "0")])
170
+ else:
171
+ await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "0")
172
+ await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "0")
@@ -74,7 +74,6 @@ class WallSwitchService(BaseService):
74
74
  return [WallSwitch(switch.raw_dict) for switch in switches]
75
75
 
76
76
  async def turn_on(self, switch: WallSwitch):
77
- logging.warn("%s", switch.single_press_type)
78
77
  if switch.single_press_type == SinglePressType.IOT:
79
78
  await self.iot_on(switch)
80
79
  else:
@@ -102,10 +102,11 @@ class PropertyIDs(Enum):
102
102
  CONTACT_STATE = "P1301"
103
103
  MOTION_STATE = "P1302"
104
104
  CAMERA_SIREN = "P1049"
105
- FLOOD_LIGHT = "P1056"
105
+ FLOOD_LIGHT = "P1056" # Also lamp socket on v3/v4 with lamp socket accessory
106
106
  SUN_MATCH = "P1528"
107
107
  MOTION_DETECTION = "P1047" # Current Motion Detection State of the Camera
108
108
  MOTION_DETECTION_TOGGLE = "P1001" # This toggles Camera Motion Detection On/Off
109
+ WCO_MOTION_DETECTION = "P1029" # Wyze cam outdoor requires both P1047 and P1029 to be set. P1029 is set via set_property_list
109
110
 
110
111
 
111
112
  class WallSwitchProps(Enum):
@@ -207,10 +208,13 @@ class HMSStatus(Enum):
207
208
  AWAY = 'away'
208
209
 
209
210
 
211
+ class DeviceMgmtToggleType:
212
+ def __init__(self, pageId, toggleId):
213
+ self.pageId = pageId
214
+ self.toggleId = toggleId
210
215
 
211
216
 
212
-
213
-
214
-
215
-
217
+ class DeviceMgmtToggleProps(Enum):
218
+ EVENT_RECORDING_TOGGLE = DeviceMgmtToggleType("cam_event_recording", "ge.motion_detect_recording")
219
+ NOTIFICATION_TOGGLE = DeviceMgmtToggleType("cam_device_notify", "ge.push_switch")
216
220
 
@@ -73,22 +73,23 @@ def create_password(password: str) -> str:
73
73
 
74
74
 
75
75
  def check_for_errors_standard(service, response_json: Dict[str, Any]) -> None:
76
- if response_json['code'] != ResponseCodes.SUCCESS.value:
77
- if response_json['code'] == ResponseCodes.PARAMETER_ERROR.value:
78
- raise ParameterError(response_json)
79
- elif response_json['code'] == ResponseCodes.ACCESS_TOKEN_ERROR.value:
76
+ response_code = response_json['code']
77
+ if response_code != ResponseCodes.SUCCESS.value:
78
+ if response_code == ResponseCodes.PARAMETER_ERROR.value:
79
+ raise ParameterError(response_code, response_json['msg'])
80
+ elif response_code == ResponseCodes.ACCESS_TOKEN_ERROR.value:
80
81
  service._auth_lib.token.expired = True
81
- raise AccessTokenError("Access Token expired, attempting to refresh")
82
- elif response_json['code'] == ResponseCodes.DEVICE_OFFLINE.value:
82
+ raise AccessTokenError(response_code, "Access Token expired, attempting to refresh")
83
+ elif response_code == ResponseCodes.DEVICE_OFFLINE.value:
83
84
  return
84
85
  else:
85
- raise UnknownApiError(response_json)
86
+ raise UnknownApiError(response_code, response_json['msg'])
86
87
 
87
88
 
88
89
  def check_for_errors_lock(service, response_json: Dict[str, Any]) -> None:
89
90
  if response_json['ErrNo'] != 0:
90
91
  if response_json.get('code') == ResponseCodes.PARAMETER_ERROR.value:
91
- raise ParameterError
92
+ raise ParameterError(response_json)
92
93
  elif response_json.get('code') == ResponseCodes.ACCESS_TOKEN_ERROR.value:
93
94
  service._auth_lib.token.expired = True
94
95
  raise AccessTokenError("Access Token expired, attempting to refresh")
@@ -96,6 +97,15 @@ def check_for_errors_lock(service, response_json: Dict[str, Any]) -> None:
96
97
  raise UnknownApiError(response_json)
97
98
 
98
99
 
100
+ def check_for_errors_devicemgmt(service, response_json: Dict[Any, Any]) -> None:
101
+ if response_json['status'] != 200:
102
+ if "InvalidTokenError>" in response_json['response']['errors'][0]['message']:
103
+ service._auth_lib.token.expired = True
104
+ raise AccessTokenError("Access Token expired, attempting to refresh")
105
+ else:
106
+ raise UnknownApiError(response_json)
107
+
108
+
99
109
  def check_for_errors_iot(service, response_json: Dict[Any, Any]) -> None:
100
110
  if response_json['code'] != 1:
101
111
  if str(response_json['code']) == ResponseCodes.ACCESS_TOKEN_ERROR.value:
@@ -267,6 +267,23 @@ class WyzeAuthLib:
267
267
  except ContentTypeError:
268
268
  _LOGGER.debug(f"Response: {response}")
269
269
  return await response.json()
270
+
271
+ async def put(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
272
+ async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
273
+ response = await _session.put(url, json=json, headers=headers, data=data)
274
+ # Relocated these below as the sanitization seems to modify the data before it goes to the post.
275
+ _LOGGER.debug("Request:")
276
+ _LOGGER.debug(f"url: {url}")
277
+ _LOGGER.debug(f"json: {self.sanitize(json)}")
278
+ _LOGGER.debug(f"headers: {self.sanitize(headers)}")
279
+ _LOGGER.debug(f"data: {self.sanitize(data)}")
280
+ # Log the response.json() if it exists, if not log the response.
281
+ try:
282
+ response_json = await response.json()
283
+ _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
284
+ except ContentTypeError:
285
+ _LOGGER.debug(f"Response: {response}")
286
+ return await response.json()
270
287
 
271
288
  async def get(self, url, headers=None, params=None) -> Dict[Any, Any]:
272
289
  async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
@@ -1,74 +0,0 @@
1
- # Copyright (c) 2021. Mulliken, LLC - All Rights Reserved
2
- # You may use, distribute and modify this code under the terms
3
- # of the attached license. You should have received a copy of
4
- # the license with this file. If not, please write to:
5
- # katie@mulliken.net to receive a copy
6
- import time
7
- from typing import Any, Dict
8
-
9
- from .const import FORD_APP_KEY
10
- from .crypto import ford_create_signature
11
-
12
-
13
- def ford_create_payload(access_token: str, payload: Dict[str, Any],
14
- url_path: str, request_method: str) -> Dict[str, Any]:
15
- payload["access_token"] = access_token
16
- payload["key"] = FORD_APP_KEY
17
- payload["timestamp"] = str(int(time.time() * 1000))
18
- payload["sign"] = ford_create_signature(url_path, request_method, payload)
19
- return payload
20
-
21
-
22
- def olive_create_get_payload(device_mac: str, keys: str) -> Dict[str, Any]:
23
- nonce = int(time.time() * 1000)
24
-
25
- return {
26
- 'keys': keys,
27
- 'did': device_mac,
28
- 'nonce': nonce
29
- }
30
-
31
-
32
- def olive_create_post_payload(device_mac: str, device_model: str, prop_key: str, value: Any) -> Dict[str, Any]:
33
- nonce = int(time.time() * 1000)
34
-
35
- return {
36
- "did": device_mac,
37
- "model": device_model,
38
- "props": {
39
- prop_key: value
40
- },
41
- "is_sub_device": 0,
42
- "nonce": str(nonce)
43
- }
44
-
45
-
46
- def olive_create_hms_payload() -> Dict[str, str]:
47
- nonce = int(time.time() * 1000)
48
-
49
- return {
50
- "group_id": "hms",
51
- "nonce": str(nonce)
52
- }
53
-
54
-
55
- def olive_create_user_info_payload() -> Dict[str, str]:
56
- nonce = int(time.time() * 1000)
57
-
58
- return {
59
- "nonce": str(nonce)
60
- }
61
-
62
-
63
- def olive_create_hms_get_payload(hms_id: str) -> Dict[str, str]:
64
- nonce = int(time.time() * 1000)
65
- return {
66
- "hms_id": hms_id,
67
- "nonce": str(nonce)
68
- }
69
-
70
-
71
- def olive_create_hms_patch_payload(hms_id: str) -> Dict[str, Any]:
72
- return {
73
- "hms_id": hms_id
74
- }
@@ -1,136 +0,0 @@
1
- # Copyright (c) 2021. Mulliken, LLC - All Rights Reserved
2
- # You may use, distribute and modify this code under the terms
3
- # of the attached license. You should have received a copy of
4
- # the license with this file. If not, please write to:
5
- # katie@mulliken.net to receive a copy
6
- import asyncio
7
- import logging
8
- import time
9
- from threading import Thread
10
- from typing import Any, List, Optional, Dict, Callable, Tuple
11
-
12
- from aiohttp import ClientOSError, ContentTypeError
13
-
14
- from ..exceptions import UnknownApiError
15
- from .base_service import BaseService
16
- from .update_manager import DeviceUpdater
17
- from ..types import Device, DeviceTypes, Event, PropertyIDs
18
- from ..utils import return_event_for_device, create_pid_pair
19
-
20
- _LOGGER = logging.getLogger(__name__)
21
-
22
-
23
- class Camera(Device):
24
- def __init__(self, dictionary: Dict[Any, Any]):
25
- super().__init__(dictionary)
26
-
27
- self.last_event: Optional[Event] = None
28
- self.last_event_ts: int = int(time.time() * 1000)
29
- self.on: bool = True
30
- self.siren: bool = False
31
- self.floodlight: bool = False
32
-
33
-
34
- class CameraService(BaseService):
35
- _updater_thread: Optional[Thread] = None
36
- _subscribers: List[Tuple[Camera, Callable[[Camera], None]]] = []
37
-
38
- async def update(self, camera: Camera):
39
- # Get updated device_params
40
- async with BaseService._update_lock:
41
- camera.device_params = await self.get_updated_params(camera.mac)
42
-
43
- # Get camera events
44
- response = await self._get_event_list(10)
45
- raw_events = response['data']['event_list']
46
- latest_events = [Event(raw_event) for raw_event in raw_events]
47
-
48
- if (event := return_event_for_device(camera, latest_events)) is not None:
49
- camera.last_event = event
50
- camera.last_event_ts = event.event_ts
51
-
52
- # Update camera state
53
- state_response: List[Tuple[PropertyIDs, Any]] = await self._get_property_list(camera)
54
- for property, value in state_response:
55
- if property is PropertyIDs.AVAILABLE:
56
- camera.available = value == "1"
57
- if property is PropertyIDs.ON:
58
- camera.on = value == "1"
59
- if property is PropertyIDs.CAMERA_SIREN:
60
- camera.siren = value == "1"
61
- if property is PropertyIDs.FLOOD_LIGHT:
62
- camera.floodlight = value == "1"
63
- if property is PropertyIDs.NOTIFICATION:
64
- camera.notify = value == "1"
65
- if property is PropertyIDs.MOTION_DETECTION:
66
- camera.motion = value == "1"
67
-
68
- return camera
69
-
70
- async def register_for_updates(self, camera: Camera, callback: Callable[[Camera], None]):
71
- loop = asyncio.get_event_loop()
72
- if not self._updater_thread:
73
- self._updater_thread = Thread(target=self.update_worker, args=[loop, ], daemon=True)
74
- self._updater_thread.start()
75
-
76
- self._subscribers.append((camera, callback))
77
-
78
- async def deregister_for_updates(self, camera: Camera):
79
- self._subscribers = [(cam, callback) for cam, callback in self._subscribers if cam.mac != camera.mac]
80
-
81
- def update_worker(self, loop):
82
- while True:
83
- if len(self._subscribers) < 1:
84
- time.sleep(0.1)
85
- else:
86
- for camera, callback in self._subscribers:
87
- try:
88
- callback(asyncio.run_coroutine_threadsafe(self.update(camera), loop).result())
89
- except UnknownApiError as e:
90
- _LOGGER.warning(f"The update method detected an UnknownApiError: {e}")
91
- except ClientOSError as e:
92
- _LOGGER.error(f"A network error was detected: {e}")
93
- except ContentTypeError as e:
94
- _LOGGER.error(f"Server returned unexpected ContentType: {e}")
95
-
96
- async def get_cameras(self) -> List[Camera]:
97
- if self._devices is None:
98
- self._devices = await self.get_object_list()
99
-
100
- cameras = [device for device in self._devices if device.type is DeviceTypes.CAMERA]
101
-
102
- return [Camera(camera.raw_dict) for camera in cameras]
103
-
104
- async def turn_on(self, camera: Camera):
105
- await self._run_action(camera, "power_on")
106
-
107
- async def turn_off(self, camera: Camera):
108
- await self._run_action(camera, "power_off")
109
-
110
- async def siren_on(self, camera: Camera):
111
- await self._run_action(camera, "siren_on")
112
-
113
- async def siren_off(self, camera: Camera):
114
- await self._run_action(camera, "siren_off")
115
-
116
- async def floodlight_on(self, camera: Camera):
117
- await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "1")
118
-
119
- async def floodlight_off(self, camera: Camera):
120
- await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "2")
121
-
122
- async def turn_on_notifications(self, camera: Camera):
123
- await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "1")
124
-
125
- async def turn_off_notifications(self, camera: Camera):
126
- await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "0")
127
-
128
- # Both properties need to be set on newer cams, older cameras seem to only react
129
- # to the first property but it doesnt hurt to set both
130
- async def turn_on_motion_detection(self, camera: Camera):
131
- await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "1")
132
- await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "1")
133
-
134
- async def turn_off_motion_detection(self, camera: Camera):
135
- await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "0")
136
- await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "0")
File without changes