wyzeapy 0.5.23__tar.gz → 0.5.25__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.
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/PKG-INFO +4 -4
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/pyproject.toml +4 -4
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/const.py +3 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/exceptions.py +1 -2
- wyzeapy-0.5.25/src/wyzeapy/payload_factory.py +570 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/base_service.py +119 -4
- wyzeapy-0.5.25/src/wyzeapy/services/camera_service.py +172 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/wall_switch_service.py +0 -1
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/types.py +9 -5
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/utils.py +18 -8
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/wyze_auth_lib.py +17 -0
- wyzeapy-0.5.23/src/wyzeapy/payload_factory.py +0 -74
- wyzeapy-0.5.23/src/wyzeapy/services/camera_service.py +0 -136
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/LICENSES/GPL-3.0-only.txt +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/__init__.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/crypto.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/__init__.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/bulb_service.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/hms_service.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/lock_service.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/sensor_service.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/switch_service.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/src/wyzeapy/services/thermostat_service.py +0 -0
- {wyzeapy-0.5.23 → wyzeapy-0.5.25}/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.
|
|
3
|
+
Version: 0.5.25
|
|
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.
|
|
14
|
-
Requires-Dist: aiohttp (>=3.
|
|
15
|
-
Requires-Dist: pycryptodome (>=3.
|
|
13
|
+
Requires-Dist: aiodns (>=3.2.0,<4.0.0)
|
|
14
|
+
Requires-Dist: aiohttp (>=3.10.8,<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.
|
|
3
|
+
version = "0.5.25"
|
|
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.
|
|
11
|
-
aiodns = "^3.
|
|
12
|
-
pycryptodome = "^3.
|
|
10
|
+
aiohttp = "^3.10.8"
|
|
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
|
|
@@ -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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|