pyezvizapi 1.0.1.7__py3-none-any.whl → 1.0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyezvizapi might be problematic. Click here for more details.
- pyezvizapi/__init__.py +11 -1
- pyezvizapi/__main__.py +406 -283
- pyezvizapi/camera.py +488 -118
- pyezvizapi/cas.py +36 -43
- pyezvizapi/client.py +785 -1345
- pyezvizapi/constants.py +6 -1
- pyezvizapi/exceptions.py +9 -9
- pyezvizapi/light_bulb.py +80 -31
- pyezvizapi/models.py +103 -0
- pyezvizapi/mqtt.py +42 -14
- pyezvizapi/test_cam_rtsp.py +95 -109
- pyezvizapi/test_mqtt.py +101 -30
- pyezvizapi/utils.py +0 -1
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/METADATA +2 -2
- pyezvizapi-1.0.1.8.dist-info/RECORD +21 -0
- pyezvizapi-1.0.1.7.dist-info/RECORD +0 -20
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/entry_points.txt +0 -0
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/top_level.txt +0 -0
pyezvizapi/__main__.py
CHANGED
|
@@ -1,25 +1,47 @@
|
|
|
1
|
-
"""pyezvizapi command line.
|
|
1
|
+
"""pyezvizapi command line.
|
|
2
|
+
|
|
3
|
+
Small utility CLI for testing and scripting Ezviz operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
2
8
|
import argparse
|
|
3
9
|
import json
|
|
4
10
|
import logging
|
|
11
|
+
from pathlib import Path
|
|
5
12
|
import sys
|
|
6
|
-
from typing import Any
|
|
13
|
+
from typing import Any, cast
|
|
7
14
|
|
|
8
15
|
import pandas as pd
|
|
9
16
|
|
|
10
17
|
from .camera import EzvizCamera
|
|
11
18
|
from .client import EzvizClient
|
|
12
|
-
from .constants import BatteryCameraWorkMode, DefenseModeType
|
|
13
|
-
from .exceptions import EzvizAuthVerificationCode
|
|
19
|
+
from .constants import BatteryCameraWorkMode, DefenseModeType, DeviceSwitchType
|
|
20
|
+
from .exceptions import EzvizAuthVerificationCode, PyEzvizError
|
|
14
21
|
from .light_bulb import EzvizLightBulb
|
|
15
|
-
|
|
22
|
+
|
|
23
|
+
_LOGGER = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _setup_logging(debug: bool) -> None:
|
|
27
|
+
"""Configure root logger for CLI usage."""
|
|
28
|
+
level = logging.DEBUG if debug else logging.INFO
|
|
29
|
+
logging.basicConfig(level=level, stream=sys.stderr, format="%(levelname)s: %(message)s")
|
|
30
|
+
if debug:
|
|
31
|
+
# Verbose requests logging in debug mode
|
|
32
|
+
requests_log = logging.getLogger("requests.packages.urllib3")
|
|
33
|
+
requests_log.setLevel(logging.DEBUG)
|
|
34
|
+
requests_log.propagate = True
|
|
16
35
|
|
|
17
36
|
|
|
18
|
-
def
|
|
19
|
-
"""
|
|
37
|
+
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
38
|
+
"""Build and parse CLI arguments.
|
|
39
|
+
|
|
40
|
+
Returns a populated `argparse.Namespace`. Pass `argv` for testing.
|
|
41
|
+
"""
|
|
20
42
|
parser = argparse.ArgumentParser(prog="pyezvizapi")
|
|
21
|
-
parser.add_argument("-u", "--username", required=
|
|
22
|
-
parser.add_argument("-p", "--password", required=
|
|
43
|
+
parser.add_argument("-u", "--username", required=False, help="Ezviz username")
|
|
44
|
+
parser.add_argument("-p", "--password", required=False, help="Ezviz Password")
|
|
23
45
|
parser.add_argument(
|
|
24
46
|
"-r",
|
|
25
47
|
"--region",
|
|
@@ -30,6 +52,20 @@ def main() -> Any:
|
|
|
30
52
|
parser.add_argument(
|
|
31
53
|
"--debug", "-d", action="store_true", help="Print debug messages to stderr"
|
|
32
54
|
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--json", action="store_true", help="Force JSON output when possible"
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--token-file",
|
|
60
|
+
type=str,
|
|
61
|
+
default="ezviz_token.json",
|
|
62
|
+
help="Path to JSON token file in the current directory (default: ezviz_token.json)",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--save-token",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="Save token to --token-file after successful login",
|
|
68
|
+
)
|
|
33
69
|
|
|
34
70
|
subparsers = parser.add_subparsers(dest="action")
|
|
35
71
|
|
|
@@ -43,6 +79,12 @@ def main() -> Any:
|
|
|
43
79
|
help="Device action to perform",
|
|
44
80
|
choices=["device", "status", "switch", "connection"],
|
|
45
81
|
)
|
|
82
|
+
parser_device.add_argument(
|
|
83
|
+
"--refresh",
|
|
84
|
+
action=argparse.BooleanOptionalAction,
|
|
85
|
+
default=True,
|
|
86
|
+
help="Refresh alarm info before composing status (default: on)",
|
|
87
|
+
)
|
|
46
88
|
|
|
47
89
|
parser_device_lights = subparsers.add_parser(
|
|
48
90
|
"devices_light", help="Get all the light bulbs"
|
|
@@ -54,6 +96,12 @@ def main() -> Any:
|
|
|
54
96
|
help="Light bulbs action to perform",
|
|
55
97
|
choices=["status"]
|
|
56
98
|
)
|
|
99
|
+
parser_device_lights.add_argument(
|
|
100
|
+
"--refresh",
|
|
101
|
+
action=argparse.BooleanOptionalAction,
|
|
102
|
+
default=True,
|
|
103
|
+
help="Refresh device data before composing status (default: on)",
|
|
104
|
+
)
|
|
57
105
|
|
|
58
106
|
parser_light = subparsers.add_parser("light", help="Light actions")
|
|
59
107
|
parser_light.add_argument("--serial", required=True, help="light bulb SERIAL")
|
|
@@ -77,7 +125,13 @@ def main() -> Any:
|
|
|
77
125
|
|
|
78
126
|
subparsers_camera = parser_camera.add_subparsers(dest="camera_action")
|
|
79
127
|
|
|
80
|
-
subparsers_camera.add_parser("status", help="Get the status of the camera")
|
|
128
|
+
parser_camera_status = subparsers_camera.add_parser("status", help="Get the status of the camera")
|
|
129
|
+
parser_camera_status.add_argument(
|
|
130
|
+
"--refresh",
|
|
131
|
+
action=argparse.BooleanOptionalAction,
|
|
132
|
+
default=True,
|
|
133
|
+
help="Refresh alarm info before composing status (default: on)",
|
|
134
|
+
)
|
|
81
135
|
subparsers_camera.add_parser("unlock-door", help="Unlock the door lock")
|
|
82
136
|
subparsers_camera.add_parser("unlock-gate", help="Unlock the gate lock")
|
|
83
137
|
parser_camera_move = subparsers_camera.add_parser("move", help="Move the camera")
|
|
@@ -161,12 +215,11 @@ def main() -> Any:
|
|
|
161
215
|
parser_camera_alarm.add_argument(
|
|
162
216
|
"--do_not_disturb",
|
|
163
217
|
required=False,
|
|
164
|
-
help=
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
Movement is still recorded even if do-not-disturb is enabled.",
|
|
218
|
+
help=(
|
|
219
|
+
"Enable/disable push notifications for motion events. "
|
|
220
|
+
"Some camera models expose this setting in the EZVIZ app, but not all. "
|
|
221
|
+
"Motion alarms are still recorded and available even when push notifications are disabled."
|
|
222
|
+
),
|
|
170
223
|
default=None,
|
|
171
224
|
type=int,
|
|
172
225
|
choices=[0, 1],
|
|
@@ -186,293 +239,363 @@ Movement is still recorded even if do-not-disturb is enabled.",
|
|
|
186
239
|
choices=[mode.name for mode in BatteryCameraWorkMode if mode is not BatteryCameraWorkMode.UNKNOWN],
|
|
187
240
|
)
|
|
188
241
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# print("--------------args")
|
|
192
|
-
# print("--------------args: %s",args)
|
|
193
|
-
# print("--------------args")
|
|
194
|
-
|
|
195
|
-
client = EzvizClient(args.username, args.password, args.region)
|
|
196
|
-
try:
|
|
197
|
-
client.login()
|
|
198
|
-
|
|
199
|
-
except EzvizAuthVerificationCode:
|
|
200
|
-
mfa_code = input("MFA code required, please input MFA code.\n")
|
|
201
|
-
client.login(sms_code=mfa_code)
|
|
202
|
-
|
|
203
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
204
|
-
print(exp)
|
|
205
|
-
|
|
206
|
-
if args.debug:
|
|
207
|
-
# You must initialize logging, otherwise you'll not see debug output.
|
|
208
|
-
logging.basicConfig()
|
|
209
|
-
logging.getLogger().setLevel(logging.DEBUG)
|
|
210
|
-
requests_log = logging.getLogger("requests.packages.urllib3")
|
|
211
|
-
requests_log.setLevel(logging.DEBUG)
|
|
212
|
-
requests_log.propagate = True
|
|
213
|
-
|
|
214
|
-
if args.action == "devices":
|
|
215
|
-
|
|
216
|
-
if args.device_action == "device":
|
|
217
|
-
try:
|
|
218
|
-
print(json.dumps(client.get_device(), indent=2))
|
|
219
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
220
|
-
print(exp)
|
|
221
|
-
finally:
|
|
222
|
-
client.close_session()
|
|
242
|
+
# Dump full pagelist for exploration
|
|
243
|
+
subparsers.add_parser("pagelist", help="Output full pagelist as JSON")
|
|
223
244
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
"name",
|
|
232
|
-
# version,
|
|
233
|
-
# upgrade_available,
|
|
234
|
-
"status",
|
|
235
|
-
"device_category",
|
|
236
|
-
"device_sub_category",
|
|
237
|
-
"sleep",
|
|
238
|
-
"privacy",
|
|
239
|
-
"audio",
|
|
240
|
-
"ir_led",
|
|
241
|
-
"state_led",
|
|
242
|
-
# follow_move,
|
|
243
|
-
# alarm_notify,
|
|
244
|
-
# alarm_schedules_enabled,
|
|
245
|
-
# alarm_sound_mod,
|
|
246
|
-
# encrypted,
|
|
247
|
-
"local_ip",
|
|
248
|
-
"local_rtsp_port",
|
|
249
|
-
"detection_sensibility",
|
|
250
|
-
"battery_level",
|
|
251
|
-
"alarm_schedules_enabled",
|
|
252
|
-
"alarm_notify",
|
|
253
|
-
"Motion_Trigger",
|
|
254
|
-
# last_alarm_time,
|
|
255
|
-
# last_alarm_pic
|
|
256
|
-
],
|
|
257
|
-
)
|
|
258
|
-
)
|
|
259
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
260
|
-
print(exp)
|
|
261
|
-
finally:
|
|
262
|
-
client.close_session()
|
|
263
|
-
|
|
264
|
-
elif args.device_action == "switch":
|
|
265
|
-
try:
|
|
266
|
-
print(json.dumps(client.get_switch(), indent=2))
|
|
267
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
268
|
-
print(exp)
|
|
269
|
-
finally:
|
|
270
|
-
client.close_session()
|
|
245
|
+
# Dump device infos mapping (optionally for a single serial)
|
|
246
|
+
parser_device_infos = subparsers.add_parser(
|
|
247
|
+
"device_infos", help="Output device infos (raw JSON), optionally filtered by serial"
|
|
248
|
+
)
|
|
249
|
+
parser_device_infos.add_argument(
|
|
250
|
+
"--serial", required=False, help="Optional serial to filter a single device"
|
|
251
|
+
)
|
|
271
252
|
|
|
272
|
-
|
|
273
|
-
try:
|
|
274
|
-
print(json.dumps(client.get_connection(), indent=2))
|
|
275
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
276
|
-
print(exp)
|
|
277
|
-
finally:
|
|
278
|
-
client.close_session()
|
|
253
|
+
return parser.parse_args(argv)
|
|
279
254
|
|
|
280
|
-
else:
|
|
281
|
-
print("Action not implemented: %s", args.device_action)
|
|
282
255
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
print(
|
|
287
|
-
pd.DataFrame.from_dict(
|
|
288
|
-
data=client.load_light_bulbs(),
|
|
289
|
-
orient="index",
|
|
290
|
-
columns=[
|
|
291
|
-
"name",
|
|
292
|
-
# "version",
|
|
293
|
-
# "upgrade_available",
|
|
294
|
-
"status",
|
|
295
|
-
"device_category",
|
|
296
|
-
"device_sub_category",
|
|
297
|
-
"local_ip",
|
|
298
|
-
"productId",
|
|
299
|
-
"is_on",
|
|
300
|
-
"brightness",
|
|
301
|
-
"color_temperature",
|
|
302
|
-
],
|
|
303
|
-
)
|
|
304
|
-
)
|
|
305
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
306
|
-
print(exp)
|
|
307
|
-
finally:
|
|
308
|
-
client.close_session()
|
|
309
|
-
|
|
310
|
-
elif args.action == "light":
|
|
311
|
-
# load light bulb object
|
|
256
|
+
def _login(client: EzvizClient) -> None:
|
|
257
|
+
"""Login if credentials are configured; skip when only a token is used."""
|
|
258
|
+
if client.account and client.password:
|
|
312
259
|
try:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
print(exp)
|
|
317
|
-
client.close_session()
|
|
318
|
-
|
|
319
|
-
if args.light_action == "toggle":
|
|
260
|
+
client.login()
|
|
261
|
+
except EzvizAuthVerificationCode:
|
|
262
|
+
mfa_code = input("MFA code required, please input MFA code.\n")
|
|
320
263
|
try:
|
|
321
|
-
|
|
322
|
-
except
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
client.close_session()
|
|
326
|
-
|
|
327
|
-
elif args.light_action == "status":
|
|
328
|
-
try:
|
|
329
|
-
print(json.dumps(light_bulb.status(), indent=2))
|
|
330
|
-
|
|
331
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
332
|
-
print(exp)
|
|
333
|
-
finally:
|
|
334
|
-
client.close_session()
|
|
335
|
-
|
|
336
|
-
elif args.action == "home_defence_mode":
|
|
337
|
-
|
|
338
|
-
if args.mode:
|
|
339
|
-
try:
|
|
340
|
-
print(
|
|
341
|
-
json.dumps(
|
|
342
|
-
client.api_set_defence_mode(
|
|
343
|
-
getattr(DefenseModeType, args.mode).value
|
|
344
|
-
),
|
|
345
|
-
indent=2,
|
|
346
|
-
)
|
|
347
|
-
)
|
|
348
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
349
|
-
print(exp)
|
|
350
|
-
finally:
|
|
351
|
-
client.close_session()
|
|
352
|
-
|
|
353
|
-
elif args.action == "mqtt":
|
|
354
|
-
|
|
355
|
-
logging.basicConfig()
|
|
356
|
-
logging.getLogger().setLevel(logging.DEBUG)
|
|
357
|
-
|
|
358
|
-
try:
|
|
359
|
-
token = client.login()
|
|
360
|
-
mqtt = MQTTClient(token)
|
|
361
|
-
mqtt.start()
|
|
362
|
-
|
|
363
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
364
|
-
print(exp)
|
|
365
|
-
finally:
|
|
366
|
-
client.close_session()
|
|
264
|
+
code_int = int(mfa_code.strip())
|
|
265
|
+
except ValueError:
|
|
266
|
+
code_int = None
|
|
267
|
+
client.login(sms_code=code_int)
|
|
367
268
|
|
|
368
|
-
elif args.action == "camera":
|
|
369
269
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
logging.debug("Camera loaded")
|
|
374
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
375
|
-
print(exp)
|
|
376
|
-
client.close_session()
|
|
270
|
+
def _write_json(obj: Any) -> None:
|
|
271
|
+
"""Write an object to stdout as pretty JSON."""
|
|
272
|
+
sys.stdout.write(json.dumps(obj, indent=2) + "\n")
|
|
377
273
|
|
|
378
|
-
if args.camera_action == "move":
|
|
379
|
-
try:
|
|
380
|
-
camera.move(args.direction, args.speed)
|
|
381
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
382
|
-
print(exp)
|
|
383
|
-
finally:
|
|
384
|
-
client.close_session()
|
|
385
274
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
390
|
-
print(exp)
|
|
391
|
-
finally:
|
|
392
|
-
client.close_session()
|
|
275
|
+
def _write_df(df: pd.DataFrame) -> None:
|
|
276
|
+
"""Write a DataFrame to stdout as a formatted table."""
|
|
277
|
+
sys.stdout.write(df.to_string() + "\n")
|
|
393
278
|
|
|
394
|
-
elif args.camera_action == "status":
|
|
395
|
-
try:
|
|
396
|
-
print(json.dumps(camera.status(), indent=2))
|
|
397
279
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
280
|
+
def _handle_devices(args: argparse.Namespace, client: EzvizClient) -> int:
|
|
281
|
+
"""Handle `devices` subcommands (device/status/switch/connection)."""
|
|
282
|
+
if args.device_action == "device":
|
|
283
|
+
_write_json(client.get_device())
|
|
284
|
+
return 0
|
|
402
285
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
286
|
+
if args.device_action == "status":
|
|
287
|
+
data = client.load_cameras(refresh=getattr(args, "refresh", True))
|
|
288
|
+
if args.json:
|
|
289
|
+
_write_json(data)
|
|
290
|
+
else:
|
|
291
|
+
# Enrich with common switch flags when available
|
|
292
|
+
def _flag_from_switch(sw: Any, code: int) -> bool | None:
|
|
293
|
+
# Accept list[{type, enable}] or dict-like {type: enable}
|
|
294
|
+
if isinstance(sw, list):
|
|
295
|
+
for item in sw:
|
|
296
|
+
if isinstance(item, dict) and item.get("type") == code:
|
|
297
|
+
val = item.get("enable")
|
|
298
|
+
return bool(val) if isinstance(val, (bool, int)) else None
|
|
299
|
+
return None
|
|
300
|
+
if isinstance(sw, dict):
|
|
301
|
+
val = sw.get(code) if code in sw else sw.get(str(code))
|
|
302
|
+
return bool(val) if isinstance(val, (bool, int)) else None
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
for payload in data.values():
|
|
306
|
+
sw = payload.get("SWITCH")
|
|
307
|
+
if sw is None:
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
# Compute all switch flags present on the device
|
|
311
|
+
flags: dict[str, bool] = {}
|
|
312
|
+
if isinstance(sw, list):
|
|
313
|
+
for item in sw:
|
|
314
|
+
if not isinstance(item, dict):
|
|
315
|
+
continue
|
|
316
|
+
t = item.get("type")
|
|
317
|
+
en = item.get("enable")
|
|
318
|
+
if not isinstance(t, int) or not isinstance(en, (bool, int)):
|
|
319
|
+
continue
|
|
320
|
+
try:
|
|
321
|
+
name = DeviceSwitchType(t).name.lower()
|
|
322
|
+
except ValueError:
|
|
323
|
+
name = f"switch_{t}"
|
|
324
|
+
flags[name] = bool(en)
|
|
325
|
+
elif isinstance(sw, dict):
|
|
326
|
+
for k, v in sw.items():
|
|
327
|
+
try:
|
|
328
|
+
t = int(k)
|
|
329
|
+
except (TypeError, ValueError):
|
|
330
|
+
continue
|
|
331
|
+
if not isinstance(v, (bool, int)):
|
|
332
|
+
continue
|
|
333
|
+
try:
|
|
334
|
+
name = DeviceSwitchType(t).name.lower()
|
|
335
|
+
except ValueError:
|
|
336
|
+
name = f"switch_{t}"
|
|
337
|
+
flags[name] = bool(v)
|
|
338
|
+
|
|
339
|
+
if flags:
|
|
340
|
+
payload["switch_flags"] = flags
|
|
341
|
+
|
|
342
|
+
# Keep legacy-friendly individual columns
|
|
343
|
+
payload["sleep"] = flags.get("sleep")
|
|
344
|
+
payload["privacy"] = flags.get("privacy")
|
|
345
|
+
payload["audio"] = flags.get("sound")
|
|
346
|
+
payload["ir_led"] = flags.get("infrared_light")
|
|
347
|
+
payload["state_led"] = flags.get("light")
|
|
348
|
+
|
|
349
|
+
df = pd.DataFrame.from_dict(
|
|
350
|
+
data=data,
|
|
351
|
+
orient="index",
|
|
352
|
+
columns=[
|
|
353
|
+
"name",
|
|
354
|
+
"status",
|
|
355
|
+
"device_category",
|
|
356
|
+
"device_sub_category",
|
|
357
|
+
"sleep",
|
|
358
|
+
"privacy",
|
|
359
|
+
"audio",
|
|
360
|
+
"ir_led",
|
|
361
|
+
"state_led",
|
|
362
|
+
"local_ip",
|
|
363
|
+
"local_rtsp_port",
|
|
364
|
+
"battery_level",
|
|
365
|
+
"alarm_schedules_enabled",
|
|
366
|
+
"alarm_notify",
|
|
367
|
+
"Motion_Trigger",
|
|
368
|
+
],
|
|
369
|
+
)
|
|
370
|
+
_write_df(df)
|
|
371
|
+
return 0
|
|
372
|
+
|
|
373
|
+
if args.device_action == "switch":
|
|
374
|
+
_write_json(client.get_switch())
|
|
375
|
+
return 0
|
|
376
|
+
|
|
377
|
+
if args.device_action == "connection":
|
|
378
|
+
_write_json(client.get_connection())
|
|
379
|
+
return 0
|
|
380
|
+
|
|
381
|
+
_LOGGER.error("Action not implemented: %s", args.device_action)
|
|
382
|
+
return 2
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _handle_devices_light(args: argparse.Namespace, client: EzvizClient) -> int:
|
|
386
|
+
"""Handle `devices_light` subcommands (status)."""
|
|
387
|
+
if args.devices_light_action == "status":
|
|
388
|
+
data = client.load_light_bulbs(refresh=getattr(args, "refresh", True))
|
|
389
|
+
if args.json:
|
|
390
|
+
_write_json(data)
|
|
391
|
+
else:
|
|
392
|
+
df = pd.DataFrame.from_dict(
|
|
393
|
+
data=data,
|
|
394
|
+
orient="index",
|
|
395
|
+
columns=[
|
|
396
|
+
"name",
|
|
397
|
+
"status",
|
|
398
|
+
"device_category",
|
|
399
|
+
"device_sub_category",
|
|
400
|
+
"local_ip",
|
|
401
|
+
"productId",
|
|
402
|
+
"is_on",
|
|
403
|
+
"brightness",
|
|
404
|
+
"color_temperature",
|
|
405
|
+
],
|
|
406
|
+
)
|
|
407
|
+
_write_df(df)
|
|
408
|
+
return 0
|
|
409
|
+
return 2
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _handle_pagelist(client: EzvizClient) -> int:
|
|
413
|
+
"""Output full pagelist (raw JSON) for exploration in editors like Notepad++."""
|
|
414
|
+
data = client._get_page_list() # noqa: SLF001
|
|
415
|
+
_write_json(data)
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _handle_device_infos(args: argparse.Namespace, client: EzvizClient) -> int:
|
|
420
|
+
"""Output device infos mapping (raw JSON), optionally filtered by serial."""
|
|
421
|
+
data = client.get_device_infos(args.serial) if args.serial else client.get_device_infos()
|
|
422
|
+
_write_json(data)
|
|
423
|
+
return 0
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _handle_light(args: argparse.Namespace, client: EzvizClient) -> int:
|
|
427
|
+
"""Handle `light` subcommands (toggle/status)."""
|
|
428
|
+
light_bulb = EzvizLightBulb(client, args.serial)
|
|
429
|
+
_LOGGER.debug("Light bulb loaded")
|
|
430
|
+
if args.light_action == "toggle":
|
|
431
|
+
light_bulb.toggle_switch()
|
|
432
|
+
return 0
|
|
433
|
+
if args.light_action == "status":
|
|
434
|
+
_write_json(light_bulb.status())
|
|
435
|
+
return 0
|
|
436
|
+
_LOGGER.error("Action not implemented for light: %s", args.light_action)
|
|
437
|
+
return 2
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _handle_home_defence_mode(args: argparse.Namespace, client: EzvizClient) -> int:
|
|
441
|
+
"""Handle `home_defence_mode` subcommands (set mode)."""
|
|
442
|
+
if args.mode:
|
|
443
|
+
res = client.api_set_defence_mode(getattr(DefenseModeType, args.mode).value)
|
|
444
|
+
_write_json(res)
|
|
445
|
+
return 0
|
|
446
|
+
return 2
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _handle_mqtt(_: argparse.Namespace, client: EzvizClient) -> int:
|
|
450
|
+
"""Connect to MQTT push notifications using current session token."""
|
|
451
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
452
|
+
client.login()
|
|
453
|
+
mqtt = client.get_mqtt_client()
|
|
454
|
+
mqtt.connect()
|
|
455
|
+
return 0
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _handle_camera(args: argparse.Namespace, client: EzvizClient) -> int:
|
|
459
|
+
"""Handle `camera` subcommands (status/move/unlock/switch/alarm/select)."""
|
|
460
|
+
camera = EzvizCamera(client, args.serial)
|
|
461
|
+
_LOGGER.debug("Camera loaded")
|
|
462
|
+
|
|
463
|
+
if args.camera_action == "move":
|
|
464
|
+
camera.move(args.direction, args.speed)
|
|
465
|
+
return 0
|
|
466
|
+
|
|
467
|
+
if args.camera_action == "move_coords":
|
|
468
|
+
camera.move_coordinates(args.x, args.y)
|
|
469
|
+
return 0
|
|
470
|
+
|
|
471
|
+
if args.camera_action == "status":
|
|
472
|
+
_write_json(camera.status(refresh=getattr(args, "refresh", True)))
|
|
473
|
+
return 0
|
|
474
|
+
|
|
475
|
+
if args.camera_action == "unlock-door":
|
|
476
|
+
camera.door_unlock()
|
|
477
|
+
return 0
|
|
478
|
+
|
|
479
|
+
if args.camera_action == "unlock-gate":
|
|
480
|
+
camera.gate_unlock()
|
|
481
|
+
return 0
|
|
482
|
+
|
|
483
|
+
if args.camera_action == "switch":
|
|
484
|
+
if args.switch == "ir":
|
|
485
|
+
camera.switch_device_ir_led(args.enable)
|
|
486
|
+
elif args.switch == "state":
|
|
487
|
+
sys.stdout.write(str(args.enable) + "\n")
|
|
488
|
+
camera.switch_device_state_led(args.enable)
|
|
489
|
+
elif args.switch == "audio":
|
|
490
|
+
camera.switch_device_audio(args.enable)
|
|
491
|
+
elif args.switch == "privacy":
|
|
492
|
+
camera.switch_privacy_mode(args.enable)
|
|
493
|
+
elif args.switch == "sleep":
|
|
494
|
+
camera.switch_sleep_mode(args.enable)
|
|
495
|
+
elif args.switch == "follow_move":
|
|
496
|
+
camera.switch_follow_move(args.enable)
|
|
497
|
+
elif args.switch == "sound_alarm":
|
|
498
|
+
camera.switch_sound_alarm(args.enable + 1)
|
|
499
|
+
else:
|
|
500
|
+
_LOGGER.error("Unknown switch: %s", args.switch)
|
|
501
|
+
return 2
|
|
502
|
+
return 0
|
|
503
|
+
|
|
504
|
+
if args.camera_action == "alarm":
|
|
505
|
+
if args.sound is not None:
|
|
506
|
+
camera.alarm_sound(args.sound)
|
|
507
|
+
if args.notify is not None:
|
|
508
|
+
camera.alarm_notify(args.notify)
|
|
509
|
+
if args.sensibility is not None:
|
|
510
|
+
camera.alarm_detection_sensibility(args.sensibility)
|
|
511
|
+
if args.do_not_disturb is not None:
|
|
512
|
+
camera.do_not_disturb(args.do_not_disturb)
|
|
513
|
+
if args.schedule is not None:
|
|
514
|
+
camera.change_defence_schedule(args.schedule)
|
|
515
|
+
return 0
|
|
516
|
+
|
|
517
|
+
if args.camera_action == "select":
|
|
518
|
+
if args.battery_work_mode is not None:
|
|
519
|
+
camera.set_battery_camera_work_mode(
|
|
520
|
+
getattr(BatteryCameraWorkMode, args.battery_work_mode)
|
|
521
|
+
)
|
|
522
|
+
return 0
|
|
523
|
+
return 2
|
|
524
|
+
|
|
525
|
+
_LOGGER.error("Action not implemented, try running with -h switch for help")
|
|
526
|
+
return 2
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _load_token_file(path: str | None) -> dict[str, Any] | None:
|
|
530
|
+
"""Load a token dictionary from `path` if it exists; else return None."""
|
|
531
|
+
if not path:
|
|
532
|
+
return None
|
|
533
|
+
p = Path(path)
|
|
534
|
+
if not p.exists():
|
|
535
|
+
return None
|
|
536
|
+
try:
|
|
537
|
+
return cast(dict[str, Any], json.loads(p.read_text(encoding="utf-8")))
|
|
538
|
+
except (OSError, json.JSONDecodeError): # pragma: no cover - tolerate malformed file
|
|
539
|
+
_LOGGER.warning("Failed to read token file: %s", p)
|
|
540
|
+
return None
|
|
411
541
|
|
|
412
|
-
elif args.camera_action == "unlock-gate":
|
|
413
|
-
try:
|
|
414
|
-
camera.gate_unlock()
|
|
415
542
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
543
|
+
def _save_token_file(path: str | None, token: dict[str, Any]) -> None:
|
|
544
|
+
"""Persist the token dictionary to `path` in JSON format."""
|
|
545
|
+
if not path:
|
|
546
|
+
return
|
|
547
|
+
p = Path(path)
|
|
548
|
+
try:
|
|
549
|
+
p.write_text(json.dumps(token, indent=2), encoding="utf-8")
|
|
550
|
+
_LOGGER.info("Saved token to %s", p)
|
|
551
|
+
except OSError: # pragma: no cover - filesystem issues
|
|
552
|
+
_LOGGER.warning("Failed to save token file: %s", p)
|
|
420
553
|
|
|
421
|
-
elif args.camera_action == "switch":
|
|
422
|
-
try:
|
|
423
|
-
if args.switch == "ir":
|
|
424
|
-
camera.switch_device_ir_led(args.enable)
|
|
425
|
-
elif args.switch == "state":
|
|
426
|
-
print(args.enable)
|
|
427
|
-
camera.switch_device_state_led(args.enable)
|
|
428
|
-
elif args.switch == "audio":
|
|
429
|
-
camera.switch_device_audio(args.enable)
|
|
430
|
-
elif args.switch == "privacy":
|
|
431
|
-
camera.switch_privacy_mode(args.enable)
|
|
432
|
-
elif args.switch == "sleep":
|
|
433
|
-
camera.switch_sleep_mode(args.enable)
|
|
434
|
-
elif args.switch == "follow_move":
|
|
435
|
-
camera.switch_follow_move(args.enable)
|
|
436
|
-
elif args.switch == "sound_alarm":
|
|
437
|
-
# Map 0|1 enable flog to operation type: 1 for off and 2 for on.
|
|
438
|
-
camera.switch_sound_alarm(args.enable + 1)
|
|
439
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
440
|
-
print(exp)
|
|
441
|
-
finally:
|
|
442
|
-
client.close_session()
|
|
443
|
-
|
|
444
|
-
elif args.camera_action == "alarm":
|
|
445
|
-
try:
|
|
446
|
-
if args.sound is not None:
|
|
447
|
-
camera.alarm_sound(args.sound)
|
|
448
|
-
if args.notify is not None:
|
|
449
|
-
camera.alarm_notify(args.notify)
|
|
450
|
-
if args.sensibility is not None:
|
|
451
|
-
camera.alarm_detection_sensibility(args.sensibility)
|
|
452
|
-
if args.do_not_disturb is not None:
|
|
453
|
-
camera.do_not_disturb(args.do_not_disturb)
|
|
454
|
-
if args.schedule is not None:
|
|
455
|
-
camera.change_defence_schedule(args.schedule)
|
|
456
|
-
except Exception as exp: # pylint: disable=broad-except
|
|
457
|
-
print(exp)
|
|
458
|
-
finally:
|
|
459
|
-
client.close_session()
|
|
460
|
-
|
|
461
|
-
elif args.camera_action == "select":
|
|
462
|
-
try:
|
|
463
|
-
if args.battery_work_mode is not None:
|
|
464
|
-
camera.set_battery_camera_work_mode(getattr(BatteryCameraWorkMode, args.battery_work_mode))
|
|
465
554
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
555
|
+
def main(argv: list[str] | None = None) -> int:
|
|
556
|
+
"""CLI entry point."""
|
|
557
|
+
args = _parse_args(argv)
|
|
558
|
+
_setup_logging(args.debug)
|
|
470
559
|
|
|
471
|
-
|
|
472
|
-
|
|
560
|
+
token = _load_token_file(args.token_file)
|
|
561
|
+
if not token and (not args.username or not args.password):
|
|
562
|
+
_LOGGER.error("Provide --token-file (existing) or --username/--password")
|
|
563
|
+
return 2
|
|
473
564
|
|
|
565
|
+
client = EzvizClient(args.username, args.password, args.region, token=token)
|
|
566
|
+
try:
|
|
567
|
+
_login(client)
|
|
568
|
+
|
|
569
|
+
if args.action == "devices":
|
|
570
|
+
return _handle_devices(args, client)
|
|
571
|
+
if args.action == "devices_light":
|
|
572
|
+
return _handle_devices_light(args, client)
|
|
573
|
+
if args.action == "light":
|
|
574
|
+
return _handle_light(args, client)
|
|
575
|
+
if args.action == "home_defence_mode":
|
|
576
|
+
return _handle_home_defence_mode(args, client)
|
|
577
|
+
if args.action == "mqtt":
|
|
578
|
+
return _handle_mqtt(args, client)
|
|
579
|
+
if args.action == "camera":
|
|
580
|
+
return _handle_camera(args, client)
|
|
581
|
+
if args.action == "pagelist":
|
|
582
|
+
return _handle_pagelist(client)
|
|
583
|
+
if args.action == "device_infos":
|
|
584
|
+
return _handle_device_infos(args, client)
|
|
585
|
+
|
|
586
|
+
except PyEzvizError as exp:
|
|
587
|
+
_LOGGER.error("%s", exp)
|
|
588
|
+
return 1
|
|
589
|
+
except KeyboardInterrupt:
|
|
590
|
+
_LOGGER.error("Interrupted")
|
|
591
|
+
return 130
|
|
474
592
|
else:
|
|
475
|
-
|
|
593
|
+
_LOGGER.error("Action not implemented: %s", args.action)
|
|
594
|
+
return 2
|
|
595
|
+
finally:
|
|
596
|
+
if args.save_token and args.token_file:
|
|
597
|
+
_save_token_file(args.token_file, cast(dict[str, Any], client._token)) # noqa: SLF001
|
|
598
|
+
client.close_session()
|
|
476
599
|
|
|
477
600
|
|
|
478
601
|
if __name__ == "__main__":
|