hoermoles-ble-cli 0.2.0__tar.gz → 0.3.2__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.
- {hoermoles_ble_cli-0.2.0 → hoermoles_ble_cli-0.3.2}/.gitignore +3 -0
- {hoermoles_ble_cli-0.2.0 → hoermoles_ble_cli-0.3.2}/PKG-INFO +1 -1
- {hoermoles_ble_cli-0.2.0 → hoermoles_ble_cli-0.3.2}/pyproject.toml +1 -1
- {hoermoles_ble_cli-0.2.0 → hoermoles_ble_cli-0.3.2}/src/hoermoles_ble_cli/cli.py +165 -84
- hoermoles_ble_cli-0.3.2/tests/test_cli.py +90 -0
- {hoermoles_ble_cli-0.2.0 → hoermoles_ble_cli-0.3.2}/README.md +0 -0
- {hoermoles_ble_cli-0.2.0 → hoermoles_ble_cli-0.3.2}/src/hoermoles_ble_cli/__init__.py +0 -0
|
@@ -27,23 +27,26 @@ Examples (after `uv sync` in the workspace root python/):
|
|
|
27
27
|
|
|
28
28
|
uv run hoermoles-ble view-log --address F1:26:AF:CC:41:86
|
|
29
29
|
"""
|
|
30
|
+
|
|
30
31
|
import argparse
|
|
31
32
|
import asyncio
|
|
32
33
|
import time
|
|
33
34
|
|
|
34
35
|
from hoermoles_ble import (
|
|
36
|
+
GATE_ACTIONS,
|
|
37
|
+
LOG_TAG_NAMES,
|
|
38
|
+
SERVICE_TYPE_IS_TIMESTAMP,
|
|
39
|
+
SERVICE_TYPE_NAMES,
|
|
35
40
|
Credentials,
|
|
36
41
|
DeviceInfo,
|
|
37
42
|
DriveMenuTable,
|
|
38
43
|
HoermannClient,
|
|
39
|
-
LOG_TAG_NAMES,
|
|
40
44
|
PropertiesRejected,
|
|
41
45
|
RegistrationTimeout,
|
|
42
|
-
GATE_ACTIONS,
|
|
43
|
-
SERVICE_TYPE_IS_TIMESTAMP,
|
|
44
|
-
SERVICE_TYPE_NAMES,
|
|
45
46
|
default_credentials_path,
|
|
47
|
+
find_qr_for_address,
|
|
46
48
|
get_device_info,
|
|
49
|
+
known_qr_serial_map,
|
|
47
50
|
list_device_infos,
|
|
48
51
|
log_timestamp_to_datetime,
|
|
49
52
|
menu_setting_for_wire_group,
|
|
@@ -51,10 +54,8 @@ from hoermoles_ble import (
|
|
|
51
54
|
parse_log_fields,
|
|
52
55
|
product_class_and_id_from_qr_prefix,
|
|
53
56
|
save_device_info,
|
|
54
|
-
scan_devices,
|
|
55
|
-
find_qr_for_address,
|
|
56
57
|
save_qr,
|
|
57
|
-
|
|
58
|
+
scan_devices,
|
|
58
59
|
serial_no_from_qr_prefix,
|
|
59
60
|
wire_group_for_menu_number,
|
|
60
61
|
)
|
|
@@ -78,12 +79,12 @@ async def cmd_scan(args) -> None:
|
|
|
78
79
|
for info in devices:
|
|
79
80
|
print(f"\n{info.address} (RSSI {info.rssi} dBm)")
|
|
80
81
|
if info.product_class is not None:
|
|
81
|
-
device_info = DeviceInfo(
|
|
82
|
-
|
|
82
|
+
device_info = DeviceInfo(
|
|
83
|
+
info.address, info.product_class, info.product_id, info.product_name, info.serial_no
|
|
84
|
+
)
|
|
83
85
|
save_device_info(device_info, config_dir=args.config_dir)
|
|
84
86
|
if info.product_name:
|
|
85
|
-
print(f" Product: {info.product_name} "
|
|
86
|
-
f"(class={info.product_class}, id={info.product_id})")
|
|
87
|
+
print(f" Product: {info.product_name} (class={info.product_class}, id={info.product_id})")
|
|
87
88
|
if info.serial_no is not None:
|
|
88
89
|
qr_status = "yes" if info.serial_no in known else f"no (0 of {len(known)} known QR codes match)"
|
|
89
90
|
print(f" QR code known: {qr_status}")
|
|
@@ -118,10 +119,13 @@ async def cmd_register(args) -> None:
|
|
|
118
119
|
with open(args.qr_file, "r") as f:
|
|
119
120
|
qr_text = f.read()
|
|
120
121
|
else:
|
|
121
|
-
log(
|
|
122
|
-
f"
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
log(
|
|
123
|
+
f"No --qr-file given, looking for a matching saved QR code for {args.address} "
|
|
124
|
+
f"(scanning for up to {args.timeout:.0f}s)..."
|
|
125
|
+
)
|
|
126
|
+
qr_text = await find_qr_for_address(
|
|
127
|
+
args.address, timeout=args.timeout, adapter=args.adapter, config_dir=args.config_dir
|
|
128
|
+
)
|
|
125
129
|
if qr_text is None:
|
|
126
130
|
raise SystemExit(
|
|
127
131
|
f"No saved QR code matches {args.address} (or the device wasn't found) - "
|
|
@@ -151,16 +155,22 @@ async def cmd_register(args) -> None:
|
|
|
151
155
|
product_info = product_class_and_id_from_qr_prefix(prefix)
|
|
152
156
|
if product_info is not None:
|
|
153
157
|
product_class, product_id = product_info
|
|
154
|
-
product_name =
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
product_name = PRODUCT_TYPE_NAMES.get((product_class, product_id)) or PRODUCT_TYPE_NAMES.get(
|
|
159
|
+
(product_class, None)
|
|
160
|
+
)
|
|
161
|
+
device_info = DeviceInfo(
|
|
162
|
+
args.address, product_class, product_id, product_name, serial_no_from_qr_prefix(prefix)
|
|
163
|
+
)
|
|
158
164
|
devices_path = save_device_info(device_info, config_dir=args.config_dir)
|
|
159
|
-
log(
|
|
160
|
-
f"
|
|
165
|
+
log(
|
|
166
|
+
f"Product type: {product_name or 'unknown'} (class={product_class}, id={product_id}), "
|
|
167
|
+
f"saved to {devices_path}"
|
|
168
|
+
)
|
|
161
169
|
else:
|
|
162
|
-
log(
|
|
163
|
-
"
|
|
170
|
+
log(
|
|
171
|
+
"Could not determine the product type from the QR code prefix (non-version-3 layout?) - "
|
|
172
|
+
"run 'scan' near the drive to detect it instead."
|
|
173
|
+
)
|
|
164
174
|
|
|
165
175
|
|
|
166
176
|
def _load_credentials(args) -> Credentials:
|
|
@@ -295,10 +305,15 @@ async def cmd_list_devices(args) -> None:
|
|
|
295
305
|
return
|
|
296
306
|
for info in infos:
|
|
297
307
|
serial = f" serial={info.serial_no}" if info.serial_no is not None else ""
|
|
298
|
-
registered =
|
|
308
|
+
registered = (
|
|
309
|
+
"registered"
|
|
310
|
+
if default_credentials_path(info.device_address, args.config_dir).exists()
|
|
299
311
|
else "not registered (no credentials)"
|
|
300
|
-
|
|
301
|
-
|
|
312
|
+
)
|
|
313
|
+
print(
|
|
314
|
+
f"{info.device_address} {info.product_name or '?'} "
|
|
315
|
+
f"(class={info.product_class}, id={info.product_id}){serial} [{registered}]"
|
|
316
|
+
)
|
|
302
317
|
|
|
303
318
|
|
|
304
319
|
async def cmd_view_log(args) -> None:
|
|
@@ -352,103 +367,169 @@ class _HelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
|
352
367
|
|
|
353
368
|
def main() -> None:
|
|
354
369
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=_HelpFormatter)
|
|
355
|
-
ap.add_argument("--adapter", default=None,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
370
|
+
ap.add_argument("--adapter", default=None, help="BlueZ adapter, e.g. hci1. Default: system default.")
|
|
371
|
+
ap.add_argument(
|
|
372
|
+
"--config-dir",
|
|
373
|
+
default=None,
|
|
374
|
+
help="Base directory for credentials. Priority: --config-dir > $HOERMOLES_CONF_DIR > .env > ~/.hoermoles",
|
|
375
|
+
)
|
|
360
376
|
sub = ap.add_subparsers(dest="command", required=True, metavar="command")
|
|
361
377
|
|
|
362
378
|
p_scan = sub.add_parser(
|
|
363
|
-
"scan",
|
|
379
|
+
"scan",
|
|
380
|
+
formatter_class=_HelpFormatter,
|
|
364
381
|
help="uv run hoermoles-ble scan",
|
|
365
|
-
description="Scan for Hoermann BlueSecur drives (no key needed)."
|
|
382
|
+
description="Scan for Hoermann BlueSecur drives (no key needed).",
|
|
383
|
+
)
|
|
366
384
|
p_scan.add_argument("--timeout", type=float, default=8.0, help="Scan duration in seconds (default: 8)")
|
|
367
385
|
p_scan.set_defaults(func=cmd_scan)
|
|
368
386
|
|
|
369
387
|
p_qr = sub.add_parser(
|
|
370
|
-
"save-qr",
|
|
388
|
+
"save-qr",
|
|
389
|
+
formatter_class=_HelpFormatter,
|
|
371
390
|
help='uv run hoermoles-ble save-qr "<QR code content>"',
|
|
372
|
-
description="Remember a QR code's content (for 'register'/'scan' without --qr-file)."
|
|
391
|
+
description="Remember a QR code's content (for 'register'/'scan' without --qr-file).",
|
|
392
|
+
)
|
|
373
393
|
p_qr.add_argument("content", help="Full QR code content as text")
|
|
374
394
|
p_qr.set_defaults(func=cmd_save_qr)
|
|
375
395
|
|
|
376
396
|
p_reg = sub.add_parser(
|
|
377
|
-
"register",
|
|
397
|
+
"register",
|
|
398
|
+
formatter_class=_HelpFormatter,
|
|
378
399
|
help="uv run hoermoles-ble register --address <MAC>",
|
|
379
|
-
description="Perform the one-time QR code registration."
|
|
400
|
+
description="Perform the one-time QR code registration.",
|
|
401
|
+
)
|
|
380
402
|
p_reg.add_argument("--address", required=True, help="BLE MAC address of the drive")
|
|
381
|
-
p_reg.add_argument(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
p_reg.add_argument(
|
|
388
|
-
|
|
403
|
+
p_reg.add_argument(
|
|
404
|
+
"--qr-file",
|
|
405
|
+
default=None,
|
|
406
|
+
help="Text file containing the QR code content. Default: look up a matching "
|
|
407
|
+
"saved QR code via scan (see save-qr)",
|
|
408
|
+
)
|
|
409
|
+
p_reg.add_argument(
|
|
410
|
+
"--timeout",
|
|
411
|
+
type=float,
|
|
412
|
+
default=8.0,
|
|
413
|
+
help="Scan duration in seconds to find the matching QR code, only relevant without --qr-file (default: 8)",
|
|
414
|
+
)
|
|
415
|
+
p_reg.add_argument(
|
|
416
|
+
"--key-file",
|
|
417
|
+
default=None,
|
|
418
|
+
help="Target file for the credentials (JSON). Default: ~/.hoermoles/credentials/<address>.json",
|
|
419
|
+
)
|
|
389
420
|
p_reg.set_defaults(func=cmd_register)
|
|
390
421
|
|
|
391
422
|
p_exec = sub.add_parser(
|
|
392
|
-
"exec",
|
|
423
|
+
"exec",
|
|
424
|
+
formatter_class=_HelpFormatter,
|
|
393
425
|
help="uv run hoermoles-ble exec --address <MAC> open|close|impulse|light|partial|ventilation",
|
|
394
|
-
description="Trigger a named gate action (open, close, impulse, light, partial, ventilation)."
|
|
395
|
-
|
|
396
|
-
p_exec.add_argument(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
426
|
+
description="Trigger a named gate action (open, close, impulse, light, partial, ventilation).",
|
|
427
|
+
)
|
|
428
|
+
p_exec.add_argument(
|
|
429
|
+
"--address",
|
|
430
|
+
default=None,
|
|
431
|
+
help="BLE MAC address (also used to find the credentials file). Default: the only/first saved credentials",
|
|
432
|
+
)
|
|
433
|
+
p_exec.add_argument(
|
|
434
|
+
"--key-file",
|
|
435
|
+
default=None,
|
|
436
|
+
help="File with saved credentials (JSON). Default: ~/.hoermoles/credentials/<address>.json",
|
|
437
|
+
)
|
|
438
|
+
p_exec.add_argument(
|
|
439
|
+
"action",
|
|
440
|
+
choices=list(GATE_ACTIONS),
|
|
441
|
+
metavar="action",
|
|
442
|
+
help="One of: " + ", ".join(GATE_ACTIONS) + ". 'impulse' is the "
|
|
443
|
+
"factory-default toggle (open/stop/close); 'open'/'close' are direct "
|
|
444
|
+
"direction commands; 'light', 'partial' and 'ventilation' depend on "
|
|
445
|
+
"the device's configuration.",
|
|
446
|
+
)
|
|
403
447
|
p_exec.set_defaults(func=cmd_exec)
|
|
404
448
|
|
|
405
449
|
p_menu_get = sub.add_parser(
|
|
406
|
-
"menu-get",
|
|
450
|
+
"menu-get",
|
|
451
|
+
formatter_class=_HelpFormatter,
|
|
407
452
|
help="uv run hoermoles-ble menu-get --address <MAC> [menu_number ...]",
|
|
408
453
|
description="Read operator menu/parameter settings - see hoermoles_ble.menu_settings for the "
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
p_menu_get.add_argument(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
454
|
+
"known products (Supramatic E4 read-verified against real hardware; others are "
|
|
455
|
+
"structurally derived, not yet verified). Without menu numbers, reads the entire "
|
|
456
|
+
"table. Needs the product type in the device registry - run 'scan' near the drive "
|
|
457
|
+
"first if 'list-devices' doesn't show it yet.",
|
|
458
|
+
)
|
|
459
|
+
p_menu_get.add_argument(
|
|
460
|
+
"--address",
|
|
461
|
+
default=None,
|
|
462
|
+
help="BLE MAC address (also used to find the credentials file). Default: the only/first saved credentials",
|
|
463
|
+
)
|
|
464
|
+
p_menu_get.add_argument(
|
|
465
|
+
"--key-file",
|
|
466
|
+
default=None,
|
|
467
|
+
help="File with saved credentials (JSON). Default: ~/.hoermoles/credentials/<address>.json",
|
|
468
|
+
)
|
|
469
|
+
p_menu_get.add_argument(
|
|
470
|
+
"menu_numbers",
|
|
471
|
+
nargs="*",
|
|
472
|
+
metavar="menu_number",
|
|
473
|
+
help="Menu number(s) to read (e.g. 25 52). Default: all known menus.",
|
|
474
|
+
)
|
|
418
475
|
p_menu_get.set_defaults(func=cmd_menu_get)
|
|
419
476
|
|
|
420
477
|
p_menu_set = sub.add_parser(
|
|
421
|
-
"menu-set",
|
|
478
|
+
"menu-set",
|
|
479
|
+
formatter_class=_HelpFormatter,
|
|
422
480
|
help="uv run hoermoles-ble menu-set --address <MAC> 25=1 52=0",
|
|
423
481
|
description="Write operator menu/parameter settings - see hoermoles_ble.menu_settings for the "
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
p_menu_set.add_argument(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
482
|
+
"known products. Needs the product type in the device registry - run 'scan' near "
|
|
483
|
+
"the drive first if 'list-devices' doesn't show it yet. NOT YET VERIFIED against "
|
|
484
|
+
"real hardware for any product - read the current value first and double check "
|
|
485
|
+
"against the printed manual.",
|
|
486
|
+
)
|
|
487
|
+
p_menu_set.add_argument(
|
|
488
|
+
"--address",
|
|
489
|
+
default=None,
|
|
490
|
+
help="BLE MAC address (also used to find the credentials file). Default: the only/first saved credentials",
|
|
491
|
+
)
|
|
492
|
+
p_menu_set.add_argument(
|
|
493
|
+
"--key-file",
|
|
494
|
+
default=None,
|
|
495
|
+
help="File with saved credentials (JSON). Default: ~/.hoermoles/credentials/<address>.json",
|
|
496
|
+
)
|
|
497
|
+
p_menu_set.add_argument(
|
|
498
|
+
"settings",
|
|
499
|
+
nargs="+",
|
|
500
|
+
metavar="menu_number=value",
|
|
501
|
+
help="One or more <menu_number>=<value> pairs, e.g. 25=1 52=0",
|
|
502
|
+
)
|
|
433
503
|
p_menu_set.set_defaults(func=cmd_menu_set)
|
|
434
504
|
|
|
435
505
|
p_list_devices = sub.add_parser(
|
|
436
|
-
"list-devices",
|
|
506
|
+
"list-devices",
|
|
507
|
+
formatter_class=_HelpFormatter,
|
|
437
508
|
help="uv run hoermoles-ble list-devices",
|
|
438
509
|
description="List every drive whose product type we know (from 'scan' and/or 'register') "
|
|
439
|
-
|
|
510
|
+
"and whether we also hold credentials for it ('registered').",
|
|
511
|
+
)
|
|
440
512
|
p_list_devices.set_defaults(func=cmd_list_devices)
|
|
441
513
|
|
|
442
514
|
p_view_log = sub.add_parser(
|
|
443
|
-
"view-log",
|
|
515
|
+
"view-log",
|
|
516
|
+
formatter_class=_HelpFormatter,
|
|
444
517
|
help="uv run hoermoles-ble view-log --address <MAC>",
|
|
445
518
|
description="Read the drive's security/access audit log (admin/user actions, "
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
p_view_log.add_argument(
|
|
451
|
-
|
|
519
|
+
"blocked attempts, clock changes) and service/diagnostics counters "
|
|
520
|
+
"(operating hours, door cycles, ...) - see hoermoles_ble.device_log. "
|
|
521
|
+
"Live-verified against a real Supramatic E4.",
|
|
522
|
+
)
|
|
523
|
+
p_view_log.add_argument(
|
|
524
|
+
"--address",
|
|
525
|
+
default=None,
|
|
526
|
+
help="BLE MAC address (also used to find the credentials file). Default: the only/first saved credentials",
|
|
527
|
+
)
|
|
528
|
+
p_view_log.add_argument(
|
|
529
|
+
"--key-file",
|
|
530
|
+
default=None,
|
|
531
|
+
help="File with saved credentials (JSON). Default: ~/.hoermoles/credentials/<address>.json",
|
|
532
|
+
)
|
|
452
533
|
p_view_log.set_defaults(func=cmd_view_log)
|
|
453
534
|
|
|
454
535
|
args = ap.parse_args()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from hoermoles_ble import devices, menu_settings
|
|
5
|
+
from hoermoles_ble.credentials import Credentials
|
|
6
|
+
|
|
7
|
+
from hoermoles_ble_cli.cli import _load_credentials, _menu_group_or_exit, _resolve_menu_table
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _args(**kwargs):
|
|
11
|
+
defaults = {"key_file": None, "address": None, "config_dir": None}
|
|
12
|
+
defaults.update(kwargs)
|
|
13
|
+
return SimpleNamespace(**defaults)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# --- _load_credentials ---------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_load_credentials_from_key_file(tmp_path):
|
|
20
|
+
creds = Credentials(device_address="AA:BB:CC:DD:EE:FF", root_id=1, root_key=b"\x00" * 32)
|
|
21
|
+
path = creds.save(config_dir=tmp_path)
|
|
22
|
+
|
|
23
|
+
loaded = _load_credentials(_args(key_file=str(path)))
|
|
24
|
+
assert loaded.device_address == "AA:BB:CC:DD:EE:FF"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_load_credentials_from_address(tmp_path):
|
|
28
|
+
creds = Credentials(device_address="AA:BB:CC:DD:EE:FF", root_id=2, root_key=b"\x01" * 32)
|
|
29
|
+
creds.save(config_dir=tmp_path)
|
|
30
|
+
|
|
31
|
+
loaded = _load_credentials(_args(address="AA:BB:CC:DD:EE:FF", config_dir=tmp_path))
|
|
32
|
+
assert loaded.root_id == 2
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_load_credentials_falls_back_to_first(tmp_path):
|
|
36
|
+
creds = Credentials(device_address="AA:BB:CC:DD:EE:FF", root_id=3, root_key=b"\x02" * 32)
|
|
37
|
+
creds.save(config_dir=tmp_path)
|
|
38
|
+
|
|
39
|
+
loaded = _load_credentials(_args(config_dir=tmp_path))
|
|
40
|
+
assert loaded.root_id == 3
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_load_credentials_exits_when_none_saved(tmp_path):
|
|
44
|
+
with pytest.raises(SystemExit):
|
|
45
|
+
_load_credentials(_args(config_dir=tmp_path))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- _menu_group_or_exit ---------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_menu_group_or_exit_found():
|
|
52
|
+
table = menu_settings.DRIVE_MENU_TABLES[0]
|
|
53
|
+
setting = table.settings[0]
|
|
54
|
+
assert _menu_group_or_exit(table, setting.menu_number) == setting.menu_group
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_menu_group_or_exit_unknown_menu_number_exits():
|
|
58
|
+
table = menu_settings.DRIVE_MENU_TABLES[0]
|
|
59
|
+
with pytest.raises(SystemExit):
|
|
60
|
+
_menu_group_or_exit(table, 999999)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --- _resolve_menu_table ---------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_resolve_menu_table_success(tmp_path):
|
|
67
|
+
creds = Credentials(device_address="F1:26:AF:CC:41:86", root_id=1, root_key=b"\x00" * 32)
|
|
68
|
+
devices.save_device_info(
|
|
69
|
+
devices.DeviceInfo(device_address=creds.device_address, product_class=2, product_id=2),
|
|
70
|
+
config_dir=tmp_path,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
table = _resolve_menu_table(_args(config_dir=tmp_path), creds)
|
|
74
|
+
assert table.settings == menu_settings.SUPRAMATIC_E4_MENU_TABLE
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_resolve_menu_table_exits_when_device_unknown(tmp_path):
|
|
78
|
+
creds = Credentials(device_address="AA:BB:CC:DD:EE:FF", root_id=1, root_key=b"\x00" * 32)
|
|
79
|
+
with pytest.raises(SystemExit):
|
|
80
|
+
_resolve_menu_table(_args(config_dir=tmp_path), creds)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_resolve_menu_table_exits_when_product_unknown(tmp_path):
|
|
84
|
+
creds = Credentials(device_address="AA:BB:CC:DD:EE:FF", root_id=1, root_key=b"\x00" * 32)
|
|
85
|
+
devices.save_device_info(
|
|
86
|
+
devices.DeviceInfo(device_address=creds.device_address, product_class=99, product_id=99),
|
|
87
|
+
config_dir=tmp_path,
|
|
88
|
+
)
|
|
89
|
+
with pytest.raises(SystemExit):
|
|
90
|
+
_resolve_menu_table(_args(config_dir=tmp_path), creds)
|
|
File without changes
|
|
File without changes
|