1
1
from __future__ import annotations
2
2
3
+ import json
3
4
import logging
4
5
import importlib
5
6
import collections
7
+ import importlib .util
6
8
7
9
import click
8
10
import zigpy .state
9
11
import zigpy .types
10
- import zigpy .config as conf
11
12
import zigpy .zdo .types
12
13
13
14
from zigpy_cli .cli import cli , click_coroutine
14
- from zigpy_cli .utils import format_bytes
15
- from zigpy_cli .common import (
16
- RADIO_TO_PYPI ,
17
- HEX_OR_DEC_INT ,
18
- RADIO_TO_PACKAGE ,
19
- RADIO_LOGGING_CONFIGS ,
20
- )
15
+ from zigpy_cli .const import RADIO_TO_PYPI , RADIO_TO_PACKAGE , RADIO_LOGGING_CONFIGS
16
+ from zigpy_cli .common import HEX_OR_DEC_INT
21
17
22
18
LOGGER = logging .getLogger (__name__ )
23
19
26
22
@click .pass_context
27
23
@click .argument ("radio" , type = click .Choice (list (RADIO_TO_PACKAGE .keys ())))
28
24
@click .argument ("port" , type = str )
25
+ @click .option ("--baudrate" , type = int , default = None )
29
26
@click_coroutine
30
- async def radio (ctx , radio , port ):
27
+ async def radio (ctx , radio , port , baudrate = None ):
31
28
# Setup logging for the radio
32
29
verbose = ctx .parent .params ["verbose" ]
33
30
logging_configs = RADIO_LOGGING_CONFIGS [radio ]
@@ -36,26 +33,25 @@ async def radio(ctx, radio, port):
36
33
for logger , level in logging_config .items ():
37
34
logging .getLogger (logger ).setLevel (level )
38
35
39
- # Import the radio library
40
36
module = RADIO_TO_PACKAGE [radio ] + ".zigbee.application"
41
37
42
- try :
43
- radio_module = importlib .import_module (module )
44
- except ImportError :
38
+ # Catching just `ImportError` masks dependency errors and is annoying
39
+ if importlib .util .find_spec (module ) is None :
45
40
raise click .ClickException (
46
41
f"Radio module for { radio !r} is not installed."
47
42
f" Install it with `pip install { RADIO_TO_PYPI [radio ]} `."
48
43
)
49
44
45
+ # Import the radio library
46
+ radio_module = importlib .import_module (module )
47
+
50
48
# Start the radio
51
49
app_cls = radio_module .ControllerApplication
52
- config = app_cls .SCHEMA (
53
- {
54
- conf .CONF_DEVICE : {
55
- conf .CONF_DEVICE_PATH : port ,
56
- },
57
- }
58
- )
50
+ config = app_cls .SCHEMA ({"device" : {"path" : port }})
51
+
52
+ if baudrate is not None :
53
+ config ["device" ]["baudrate" ] = baudrate
54
+
59
55
app = app_cls (config )
60
56
61
57
ctx .obj = app
@@ -66,36 +62,59 @@ async def radio(ctx, radio, port):
66
62
@click_coroutine
67
63
async def radio_cleanup (app ):
68
64
try :
69
- await app .pre_shutdown ()
65
+ await app .shutdown ()
70
66
except RuntimeError :
71
67
LOGGER .warning ("Caught an exception when shutting down app" , exc_info = True )
72
68
73
69
74
- def dump_app_info (app ):
75
- if app .pan_id is not None :
76
- print (f"PAN ID: 0x{ app .pan_id :04X} " )
70
+ @radio .command ()
71
+ @click .pass_obj
72
+ @click_coroutine
73
+ async def info (app ):
74
+ await app .connect ()
75
+ await app .load_network_info (load_devices = False )
76
+
77
+ print (f"PAN ID: 0x{ app .state .network_info .pan_id :04X} " )
78
+ print (f"Extended PAN ID: { app .state .network_info .extended_pan_id } " )
79
+ print (f"Channel: { app .state .network_info .channel } " )
80
+ print (f"Channel mask: { list (app .state .network_info .channel_mask )} " )
81
+ print (f"NWK update ID: { app .state .network_info .nwk_update_id } " )
82
+ print (f"Device IEEE: { app .state .node_info .ieee } " )
83
+ print (f"Device NWK: 0x{ app .state .node_info .nwk :04X} " )
84
+ print (f"Network key: { app .state .network_info .network_key .key } " )
85
+ print (f"Network key sequence: { app .state .network_info .network_key .seq } " )
86
+ print (f"Network key counter: { app .state .network_info .network_key .tx_counter } " )
77
87
78
- print (f"Extended PAN ID: { app .extended_pan_id } " )
79
- print (f"Channel: { app .channel } " )
80
88
81
- if app .channels is not None :
82
- print (f"Channel mask: { list (app .channels )} " )
89
+ @radio .command ()
90
+ @click .argument ("output" , type = click .File ("w" ))
91
+ @click .pass_obj
92
+ @click_coroutine
93
+ async def backup (app , output ):
94
+ await app .connect ()
95
+ await app .load_network_info (load_devices = True )
83
96
84
- print (f"NWK update ID: { app .nwk_update_id } " )
85
- print (f"Device IEEE: { app .ieee } " )
86
- print (f"Device NWK: 0x{ app .nwk :04X} " )
97
+ obj = zigpy .state .network_state_to_json (
98
+ network_info = app .state .network_info ,
99
+ node_info = app .state .node_info ,
100
+ )
87
101
88
- if getattr (app , "network_key" , None ) is not None :
89
- print (f"Network key: { format_bytes (app .network_key )} " )
90
- print (f"Network key sequence: { app .network_key_seq } " )
102
+ output .write (json .dumps (obj , indent = 4 ))
91
103
92
104
93
105
@radio .command ()
106
+ @click .argument ("input" , type = click .File ("r" ))
107
+ @click .option ("-c" , "--frame-counter-increment" , type = int , default = 5000 )
94
108
@click .pass_obj
95
109
@click_coroutine
96
- async def info (app ):
97
- await app .startup (auto_form = False )
98
- dump_app_info (app )
110
+ async def restore (app , frame_counter_increment , input ):
111
+ obj = json .load (input )
112
+
113
+ network_info , node_info = zigpy .state .json_to_network_state (obj )
114
+ network_info .network_key .tx_counter += frame_counter_increment
115
+
116
+ await app .connect ()
117
+ await app .write_network_info (network_info = network_info , node_info = node_info )
99
118
100
119
101
120
@radio .command ()
@@ -104,7 +123,6 @@ async def info(app):
104
123
async def form (app ):
105
124
await app .startup (auto_form = True )
106
125
await app .form_network ()
107
- dump_app_info (app )
108
126
109
127
110
128
@radio .command ()
@@ -149,14 +167,23 @@ async def energy_scan(app, nwk):
149
167
print (" + TX on 26 in North America may be with lower power due to regulations" )
150
168
print (" + Zigbee channels 15, 20, 25 fall between WiFi channels 1, 6, 11" )
151
169
print (" + Some Zigbee devices only join networks on channels 15, 20, and 25" )
170
+ print (" + Current channel is enclosed in [square brackets]" )
152
171
print ("------------------------------------------------" )
153
172
154
173
for channel , energies in channel_energies .items ():
155
174
count = sum (energies )
156
175
asterisk = "*" if channel == 26 else " "
157
176
177
+ if channel == app .state .network_info .channel :
178
+ bracket_open = "["
179
+ bracket_close = "]"
180
+ else :
181
+ bracket_open = " "
182
+ bracket_close = " "
183
+
158
184
print (
159
- f" - { channel :>02} { asterisk } { count / total :>7.2%} "
185
+ f" - { bracket_open } { channel :>02} { asterisk } { bracket_close } "
186
+ + f" { count / total :>7.2%} "
160
187
+ "#" * int (100 * count / total )
161
188
)
162
189
0 commit comments