Skip to content

Commit 49ce224

Browse files
committed
Simplify StoredDevice and serialize backup JSON only at the very end
1 parent bc86ed8 commit 49ce224

File tree

3 files changed

+123
-114
lines changed

3 files changed

+123
-114
lines changed

zigpy_znp/api.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import itertools
88
import contextlib
9+
import dataclasses
910
from collections import Counter, defaultdict
1011

1112
import zigpy.state
@@ -165,28 +166,14 @@ async def load_network_info(self, *, load_devices=False):
165166

166167
# This takes a few seconds
167168
if load_devices:
168-
for device in await security.read_devices(self):
169-
if device.is_child:
170-
network_info.children.append(
171-
zigpy.state.NodeInfo(
172-
ieee=device.ieee,
173-
nwk=device.nwk,
174-
logical_type=zdo_t.LogicalType.EndDevice,
175-
)
176-
)
169+
for dev in await security.read_devices(self):
170+
if dev.is_child:
171+
network_info.children.append(dev.node_info)
177172
else:
178-
network_info.nwk_addresses[device.ieee] = device.nwk
179-
180-
if device.aps_link_key is not None:
181-
network_info.key_table.append(
182-
zigpy.state.Key(
183-
key=device.aps_link_key,
184-
seq=0,
185-
tx_counter=device.tx_counter,
186-
rx_counter=device.rx_counter,
187-
partner_ieee=device.ieee,
188-
)
189-
)
173+
network_info.nwk_addresses[dev.node_info.ieee] = dev.node_info.nwk
174+
175+
if dev.key is not None:
176+
network_info.key_table.append(dev.key)
190177

191178
ieee = await self.nvram.osal_read(OsalNvIds.EXTADDR, item_type=t.EUI64)
192179
logical_type = await self.nvram.osal_read(
@@ -395,8 +382,10 @@ async def write_network_info(
395382

396383
for child in network_info.children or []:
397384
devices[child.ieee] = security.StoredDevice(
398-
nwk=child.nwk if child.nwk is not None else 0xFFFE,
399-
ieee=child.ieee,
385+
node_info=dataclasses.replace(
386+
child, nwk=0xFFFE if child.nwk is None else child.nwk
387+
),
388+
key=None,
400389
is_child=True,
401390
)
402391

@@ -405,26 +394,30 @@ async def write_network_info(
405394

406395
if device is None:
407396
device = security.StoredDevice(
408-
nwk=network_info.nwk_addresses.get(key.partner_ieee, 0xFFFE),
409-
ieee=key.partner_ieee,
397+
node_info=zigpy.state.NodeInfo(
398+
nwk=network_info.nwk_addresses.get(key.partner_ieee, 0xFFFE),
399+
ieee=key.partner_ieee,
400+
logical_type=None,
401+
),
402+
key=None,
410403
is_child=False,
411404
)
412405

413-
devices[key.partner_ieee] = device.replace(
414-
aps_link_key=key.key,
415-
tx_counter=key.tx_counter,
416-
rx_counter=key.rx_counter,
417-
)
406+
devices[key.partner_ieee] = device.replace(key=key)
418407

419408
LOGGER.debug("Writing children and keys")
420409

421-
await security.write_devices(
410+
new_tclk_seed = await security.write_devices(
422411
znp=self,
423412
devices=list(devices.values()),
424413
tclk_seed=tclk_seed,
425414
counter_increment=0,
426415
)
427416

417+
# If the provided TCLK seed isn't optimal, overwrite it
418+
if new_tclk_seed != tclk_seed:
419+
await self.nvram.osal_write(OsalNvIds.TCLK_SEED, new_tclk_seed)
420+
428421
if self.version == 1.2:
429422
await self.nvram.osal_write(
430423
OsalNvIds.HAS_CONFIGURED_ZSTACK1,

zigpy_znp/tools/network_backup.py

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,76 +6,100 @@
66
import logging
77
import datetime
88

9+
import zigpy.state
10+
911
import zigpy_znp
1012
import zigpy_znp.types as t
1113
from zigpy_znp.api import ZNP
1214
from zigpy_znp.tools.common import ClosableFileType, setup_parser, validate_backup_json
13-
from zigpy_znp.znp.security import read_devices
1415
from zigpy_znp.zigbee.application import ControllerApplication
1516

1617
LOGGER = logging.getLogger(__name__)
1718

1819

19-
async def backup_network(znp: ZNP) -> t.JSONType:
20-
try:
21-
await znp.load_network_info()
22-
except ValueError as e:
23-
raise RuntimeError("Failed to load network info") from e
24-
25-
devices = []
20+
def zigpy_state_to_json_backup(
21+
network_info: zigpy.state.NetworkInformation, node_info: zigpy.state.NodeInfo
22+
) -> t.JSONType:
23+
devices = {}
2624

27-
for device in await read_devices(znp):
28-
nwk = device.nwk if device.nwk != 0xFFFE else None
29-
obj = {
25+
for ieee, nwk in network_info.nwk_addresses.items():
26+
devices[ieee] = {
27+
"ieee_address": ieee.serialize()[::-1].hex(),
3028
"nwk_address": nwk.serialize()[::-1].hex(),
31-
"ieee_address": device.ieee.serialize()[::-1].hex(),
32-
"is_child": device.is_child,
29+
"is_child": False,
3330
}
3431

35-
if device.aps_link_key:
36-
obj["link_key"] = {
37-
"tx_counter": device.tx_counter,
38-
"rx_counter": device.rx_counter,
39-
"key": device.aps_link_key.serialize().hex(),
40-
}
41-
42-
devices.append(obj)
32+
for child in network_info.children:
33+
devices[child.ieee] = {
34+
"ieee_address": child.ieee.serialize()[::-1].hex(),
35+
"nwk_address": child.nwk.serialize()[::-1].hex()
36+
if child.nwk is not None
37+
else None,
38+
"is_child": True,
39+
}
4340

44-
devices.sort(key=lambda d: d["ieee_address"])
41+
for key in network_info.key_table:
42+
if key.partner_ieee not in devices:
43+
devices[key.partner_ieee] = {
44+
"ieee_address": key.partner_ieee.serialize()[::-1].hex(),
45+
"nwk_address": None,
46+
"is_child": False,
47+
}
4548

46-
now = datetime.datetime.now().astimezone()
49+
devices[key.partner_ieee]["link_key"] = {
50+
"key": key.key.serialize().hex(),
51+
"tx_counter": key.tx_counter,
52+
"rx_counter": key.rx_counter,
53+
}
4754

48-
obj = {
55+
return {
4956
"metadata": {
5057
"version": 1,
5158
"format": "zigpy/open-coordinator-backup",
52-
"source": f"zigpy-znp@{zigpy_znp.__version__}",
53-
"internal": {
54-
"creation_time": now.isoformat(timespec="seconds"),
55-
"zstack": {
56-
"version": znp.version,
57-
},
58-
},
59+
"source": None,
60+
"internal": None,
5961
},
60-
"coordinator_ieee": znp.node_info.ieee.serialize()[::-1].hex(),
61-
"pan_id": znp.network_info.pan_id.serialize()[::-1].hex(),
62-
"extended_pan_id": znp.network_info.extended_pan_id.serialize()[::-1].hex(),
63-
"nwk_update_id": znp.network_info.nwk_update_id,
64-
"security_level": znp.network_info.security_level,
65-
"channel": znp.network_info.channel,
66-
"channel_mask": list(znp.network_info.channel_mask),
62+
"coordinator_ieee": node_info.ieee.serialize()[::-1].hex(),
63+
"pan_id": network_info.pan_id.serialize()[::-1].hex(),
64+
"extended_pan_id": network_info.extended_pan_id.serialize()[::-1].hex(),
65+
"nwk_update_id": network_info.nwk_update_id,
66+
"security_level": network_info.security_level,
67+
"channel": network_info.channel,
68+
"channel_mask": list(network_info.channel_mask),
6769
"network_key": {
68-
"key": znp.network_info.network_key.key.serialize().hex(),
69-
"sequence_number": znp.network_info.network_key.seq,
70-
"frame_counter": znp.network_info.network_key.tx_counter,
70+
"key": network_info.network_key.key.serialize().hex(),
71+
"sequence_number": network_info.network_key.seq,
72+
"frame_counter": network_info.network_key.tx_counter,
73+
},
74+
"devices": sorted(devices.values(), key=lambda d: d["ieee_address"]),
75+
}
76+
77+
78+
async def backup_network(znp: ZNP) -> t.JSONType:
79+
try:
80+
await znp.load_network_info()
81+
except ValueError as e:
82+
raise RuntimeError("Failed to load network info") from e
83+
84+
await znp.load_network_info(load_devices=True)
85+
86+
obj = zigpy_state_to_json_backup(
87+
network_info=znp.network_info,
88+
node_info=znp.node_info,
89+
)
90+
91+
now = datetime.datetime.now().astimezone()
92+
93+
obj["metadata"]["source"] = f"zigpy-znp@{zigpy_znp.__version__}"
94+
obj["metadata"]["internal"] = {
95+
"creation_time": now.isoformat(timespec="seconds"),
96+
"zstack": {
97+
"version": znp.version,
7198
},
72-
"devices": devices,
7399
}
74100

75-
if znp.network_info.stack_specific.get("zstack", {}).get("tclk_seed"):
76-
obj.setdefault("stack_specific", {}).setdefault("zstack", {})[
77-
"tclk_seed"
78-
] = znp.network_info.stack_specific["zstack"]["tclk_seed"]
101+
if znp.network_info.stack_specific:
102+
obj["stack_specific"] = znp.network_info.stack_specific
79103

80104
# Ensure our generated backup is valid
81105
validate_backup_json(obj)

zigpy_znp/znp/security.py

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,8 @@
1616

1717
@dataclasses.dataclass(frozen=True)
1818
class StoredDevice:
19-
ieee: t.EUI64
20-
nwk: t.NWK
21-
22-
aps_link_key: t.KeyData = None
23-
24-
tx_counter: t.uint32_t = None
25-
rx_counter: t.uint32_t = None
26-
19+
node_info: zigpy.state.NodeInfo
20+
key: zigpy.state.Key | None
2721
is_child: bool = False
2822

2923
def replace(self, **kwargs) -> StoredDevice:
@@ -63,7 +57,7 @@ def iter_seed_candidates(
6357
) -> typing.Iterable[tuple[int, t.KeyData]]:
6458
for ieee, key in ieees_and_keys:
6559
# Derive a seed from each candidate. All rotations of a seed are equivalent.
66-
tclk_seed = compute_tclk_seed(ieee, key, 0)
60+
tclk_seed = t.KeyData(compute_tclk_seed(ieee, key, 0))
6761

6862
# And see how many other keys share this same seed
6963
count = count_seed_matches(ieees_and_keys, tclk_seed)
@@ -276,8 +270,12 @@ async def read_devices(znp: ZNP) -> typing.Sequence[StoredDevice]:
276270
t.AddrMgrUserType.Security,
277271
):
278272
devices[entry.extAddr] = StoredDevice(
279-
ieee=entry.extAddr,
280-
nwk=entry.nwkAddr,
273+
node_info=zigpy.state.NodeInfo(
274+
nwk=entry.nwkAddr,
275+
ieee=entry.extAddr,
276+
logical_type=None,
277+
),
278+
key=None,
281279
is_child=bool(entry.type & t.AddrMgrUserType.Assoc),
282280
)
283281
else:
@@ -294,11 +292,7 @@ async def read_devices(znp: ZNP) -> typing.Sequence[StoredDevice]:
294292
)
295293
continue
296294

297-
devices[key.partner_ieee] = devices[key.partner_ieee].replace(
298-
tx_counter=key.tx_counter,
299-
rx_counter=key.rx_counter,
300-
aps_link_key=key.key,
301-
)
295+
devices[key.partner_ieee] = devices[key.partner_ieee].replace(key=key)
302296

303297
async for key in read_unhashed_link_keys(znp, addr_mgr):
304298
if key.partner_ieee not in devices:
@@ -311,11 +305,7 @@ async def read_devices(znp: ZNP) -> typing.Sequence[StoredDevice]:
311305
)
312306
continue
313307

314-
devices[key.partner_ieee] = devices[key.partner_ieee].replace(
315-
tx_counter=key.tx_counter,
316-
rx_counter=key.rx_counter,
317-
aps_link_key=key.key,
318-
)
308+
devices[key.partner_ieee] = devices[key.partner_ieee].replace(key=key)
319309

320310
return list(devices.values())
321311

@@ -328,11 +318,11 @@ async def write_addr_manager_entries(
328318
for dev in devices:
329319
entry = t.AddrMgrEntry(
330320
type=t.AddrMgrUserType.Default,
331-
nwkAddr=dev.nwk,
332-
extAddr=dev.ieee,
321+
nwkAddr=dev.node_info.nwk,
322+
extAddr=dev.node_info.ieee,
333323
)
334324

335-
if dev.aps_link_key:
325+
if dev.key is not None:
336326
entry.type |= t.AddrMgrUserType.Security
337327

338328
if dev.is_child:
@@ -366,9 +356,9 @@ async def write_devices(
366356
znp: ZNP,
367357
devices: typing.Sequence[StoredDevice],
368358
counter_increment: t.uint32_t = 2500,
369-
tclk_seed: bytes = None,
370-
) -> None:
371-
ieees_and_keys = [(d.ieee, d.aps_link_key) for d in devices if d.aps_link_key]
359+
tclk_seed: t.KeyData = None,
360+
) -> t.KeyData:
361+
ieees_and_keys = [(d.node_info.ieee, d.key.key) for d in devices if d.key]
372362

373363
# Find the tclk_seed that maximizes the number of keys that can be derived from it
374364
if ieees_and_keys:
@@ -397,19 +387,19 @@ async def write_devices(
397387
aps_key_data_table = []
398388
link_key_table = t.APSLinkKeyTable()
399389

400-
for index, device in enumerate(devices):
401-
if not device.aps_link_key:
390+
for index, dev in enumerate(devices):
391+
if dev.key is None:
402392
continue
403393

404-
shift = find_key_shift(device.ieee, device.aps_link_key, tclk_seed)
394+
shift = find_key_shift(dev.node_info.ieee, dev.key.key, tclk_seed)
405395

406396
if shift is not None:
407397
# Hashed link keys can be written into the TCLK table
408398
hashed_link_key_table.append(
409399
t.TCLKDevEntry(
410-
txFrmCntr=device.tx_counter + counter_increment,
411-
rxFrmCntr=device.rx_counter,
412-
extAddr=device.ieee,
400+
txFrmCntr=dev.key.tx_counter + counter_increment,
401+
rxFrmCntr=dev.key.rx_counter,
402+
extAddr=dev.node_info.ieee,
413403
keyAttributes=t.KeyAttributes.VERIFIED_KEY,
414404
keyType=t.KeyType.NONE,
415405
SeedShift_IcIndex=shift,
@@ -419,9 +409,9 @@ async def write_devices(
419409
# Unhashed link keys are written to another table
420410
aps_key_data_table.append(
421411
t.APSKeyDataTableEntry(
422-
Key=device.aps_link_key,
423-
TxFrameCounter=device.tx_counter + counter_increment,
424-
RxFrameCounter=device.rx_counter,
412+
Key=dev.key.key,
413+
TxFrameCounter=dev.key.tx_counter + counter_increment,
414+
RxFrameCounter=dev.key.rx_counter,
425415
)
426416
)
427417

@@ -463,7 +453,7 @@ async def write_devices(
463453
await write_addr_manager_entries(znp, devices)
464454

465455
if old_link_key_table is None:
466-
return
456+
return tclk_seed
467457

468458
await znp.nvram.osal_write(OsalNvIds.APS_LINK_KEY_TABLE, new_link_key_table_value)
469459

@@ -508,3 +498,5 @@ async def write_devices(
508498
values=aps_key_data_table,
509499
fill_value=aps_key_data_fill_value,
510500
)
501+
502+
return tclk_seed

0 commit comments

Comments
 (0)