Skip to content

Commit 11ec842

Browse files
committed
Fix broken empty address manager entries on startup on affected devices
1 parent 06af078 commit 11ec842

File tree

6 files changed

+117
-26
lines changed

6 files changed

+117
-26
lines changed

tests/application/test_startup.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22

3+
import zigpy_znp.const as const
34
import zigpy_znp.types as t
45
import zigpy_znp.config as conf
56
import zigpy_znp.commands as c
@@ -267,3 +268,31 @@ async def test_auto_form_necessary(device, make_application, mocker):
267268
assert nvram[OsalNvIds.ZDO_DIRECT_CB] == t.Bool(True).serialize()
268269

269270
await app.shutdown()
271+
272+
273+
@pytest.mark.parametrize("device", [FormedZStack1CC2531, FormedZStack3CC2531])
274+
async def test_addrmgr_rewrite_fix(device, make_application, mocker):
275+
app, znp_server = make_application(server_cls=device)
276+
277+
nvram = znp_server._nvram[ExNvIds.LEGACY]
278+
279+
assert (
280+
nvram[OsalNvIds.ADDRMGR].count(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1.serialize())
281+
> 2
282+
)
283+
nvram[OsalNvIds.ADDRMGR] = nvram[OsalNvIds.ADDRMGR].replace(
284+
const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1.serialize(),
285+
const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3.serialize(),
286+
2,
287+
)
288+
289+
assert const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1.serialize() in nvram[OsalNvIds.ADDRMGR]
290+
assert const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3.serialize() in nvram[OsalNvIds.ADDRMGR]
291+
292+
await app.startup()
293+
await app.shutdown()
294+
295+
assert const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1.serialize() in nvram[OsalNvIds.ADDRMGR]
296+
assert (
297+
const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3.serialize() not in nvram[OsalNvIds.ADDRMGR]
298+
)

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,7 @@ def _create_network_nvram(self):
978978
super()._create_network_nvram()
979979
self._nvram[ExNvIds.LEGACY][OsalNvIds.APS_LINK_KEY_TABLE] = b"\xFF" * 20
980980
self._nvram[ExNvIds.ADDRMGR] = {
981-
addr: self.nvram_serialize(const.EMPTY_ADDR_MGR_ENTRY)
981+
addr: self.nvram_serialize(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3)
982982
for addr in range(0x0000, 0x0100 + 1)
983983
}
984984

@@ -1094,7 +1094,7 @@ def _create_network_nvram(self):
10941094
super()._create_network_nvram()
10951095
self._nvram[ExNvIds.LEGACY][OsalNvIds.APS_LINK_KEY_TABLE] = b"\xFF" * 17
10961096
self._nvram[ExNvIds.LEGACY][OsalNvIds.ADDRMGR] = 124 * self.nvram_serialize(
1097-
const.EMPTY_ADDR_MGR_ENTRY
1097+
const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1
10981098
)
10991099

11001100
def create_nib(self, _=None):

zigpy_znp/const.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
)
1313
ZSTACK_CONFIGURE_SUCCESS = t.uint8_t(0x55)
1414

15-
EMPTY_ADDR_MGR_ENTRY = t.AddrMgrEntry(
15+
EMPTY_ADDR_MGR_ENTRY_ZSTACK1 = t.AddrMgrEntry(
16+
type=t.AddrMgrUserType.Default,
17+
nwkAddr=0xFFFF,
18+
extAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"),
19+
)
20+
21+
EMPTY_ADDR_MGR_ENTRY_ZSTACK3 = t.AddrMgrEntry(
1622
type=t.AddrMgrUserType(0xFF),
1723
nwkAddr=0xFFFF,
1824
extAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"),

zigpy_znp/types/nvids.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class OsalNvIds(BaseNvIds):
3939
# Introduced by zigbeer/zigbee-shepherd and now used by Zigbee2MQTT
4040
HAS_CONFIGURED_ZSTACK1 = 0x0F00
4141
HAS_CONFIGURED_ZSTACK3 = 0x0060
42+
ZIGPY_ZNP_MIGRATION_ID = 0x0FEE
4243

4344
# OSAL NV item IDs
4445
EXTADDR = 0x0001

zigpy_znp/zigbee/application.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import zigpy_znp.config as conf
3131
import zigpy_znp.commands as c
3232
from zigpy_znp.api import ZNP
33+
from zigpy_znp.znp import security
3334
from zigpy_znp.utils import combine_concurrent_calls
3435
from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse
3536
from zigpy_znp.types.nvids import OsalNvIds
@@ -186,6 +187,9 @@ async def _startup(self, auto_form=False, force_form=False, read_only=False):
186187
self._znp = znp
187188
self._znp.set_application(self)
188189

190+
if not read_only and not force_form:
191+
await self._migrate_nvram()
192+
189193
self._bind_callbacks()
190194

191195
# Next, read out the NVRAM item that Zigbee2MQTT writes when it has configured
@@ -928,6 +932,53 @@ async def _set_led_mode(self, *, led: t.uint8_t, mode: c.util.LEDMode) -> None:
928932
except (asyncio.TimeoutError, CommandNotRecognized):
929933
LOGGER.info("This build of Z-Stack does not appear to support LED control")
930934

935+
async def _migrate_nvram(self):
936+
"""
937+
Migrates NVRAM entries using the `ZIGPY_ZNP_MIGRATION_ID` NVRAM item.
938+
"""
939+
940+
try:
941+
migration_id = await self._znp.nvram.osal_read(
942+
OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, item_type=t.uint8_t
943+
)
944+
except KeyError:
945+
migration_id = 0
946+
947+
initial_migration_id = migration_id
948+
949+
# Migration 1: empty `ADDRMGR` entries are version-dependent
950+
if migration_id < 1:
951+
try:
952+
entries = await security.read_addr_manager_entries(self._znp)
953+
except KeyError:
954+
pass
955+
else:
956+
fixed_entries = []
957+
958+
for entry in entries:
959+
if entry not in (
960+
const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1,
961+
const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3,
962+
):
963+
fixed_entries.append(entry)
964+
elif self._znp.version == 3.30:
965+
fixed_entries.append(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3)
966+
else:
967+
fixed_entries.append(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1)
968+
969+
if entries != fixed_entries:
970+
LOGGER.warning("Fixing broken address manager entries")
971+
await security.write_addr_manager_entries(self._znp, fixed_entries)
972+
973+
migration_id = 1
974+
975+
await self._znp.nvram.osal_write(
976+
OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, t.uint8_t(migration_id), create=True
977+
)
978+
979+
if initial_migration_id != migration_id:
980+
await self._znp.reset()
981+
931982
async def _write_stack_settings(self, *, reset_if_changed: bool) -> None:
932983
"""
933984
Writes network-independent Z-Stack settings to NVRAM.

zigpy_znp/znp/security.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ async def write_tc_frame_counter(
153153
)
154154

155155

156-
async def read_addr_mgr_entries(znp: ZNP) -> typing.Sequence[t.AddrMgrEntry]:
156+
async def read_addr_manager_entries(znp: ZNP) -> typing.Sequence[t.AddrMgrEntry]:
157157
if znp.version >= 3.30:
158158
entries = [
159159
entry
@@ -259,7 +259,7 @@ async def read_devices(znp: ZNP) -> typing.Sequence[StoredDevice]:
259259
if znp.version > 1.2:
260260
tclk_seed = await znp.nvram.osal_read(OsalNvIds.TCLK_SEED, item_type=t.KeyData)
261261

262-
addr_mgr = await read_addr_mgr_entries(znp)
262+
addr_mgr = await read_addr_manager_entries(znp)
263263
devices = {}
264264

265265
for entry in addr_mgr:
@@ -317,30 +317,13 @@ async def read_devices(znp: ZNP) -> typing.Sequence[StoredDevice]:
317317

318318

319319
async def write_addr_manager_entries(
320-
znp: ZNP, devices: typing.Sequence[StoredDevice]
320+
znp: ZNP, entries: typing.Sequence[t.AddrMgrEntry]
321321
) -> None:
322-
entries = []
323-
324-
for dev in devices:
325-
entry = t.AddrMgrEntry(
326-
type=t.AddrMgrUserType.Default,
327-
nwkAddr=dev.node_info.nwk,
328-
extAddr=dev.node_info.ieee,
329-
)
330-
331-
if dev.key is not None:
332-
entry.type |= t.AddrMgrUserType.Security
333-
334-
if dev.is_child:
335-
entry.type |= t.AddrMgrUserType.Assoc
336-
337-
entries.append(entry)
338-
339322
if znp.version >= 3.30:
340323
await znp.nvram.write_table(
341324
item_id=ExNvIds.ADDRMGR,
342325
values=entries,
343-
fill_value=const.EMPTY_ADDR_MGR_ENTRY,
326+
fill_value=const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3,
344327
)
345328
return
346329

@@ -349,7 +332,11 @@ async def write_addr_manager_entries(
349332
old_entries = await znp.nvram.osal_read(
350333
OsalNvIds.ADDRMGR, item_type=t.AddressManagerTable
351334
)
352-
new_entries = len(old_entries) * [const.EMPTY_ADDR_MGR_ENTRY]
335+
336+
if znp.version >= 3.30:
337+
new_entries = len(old_entries) * [const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3]
338+
else:
339+
new_entries = len(old_entries) * [const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1]
353340

354341
# Purposefully throw an `IndexError` if we are trying to write too many entries
355342
for index, entry in enumerate(entries):
@@ -449,8 +436,25 @@ async def write_devices(
449436
if len(new_link_key_table_value) > len(old_link_key_table):
450437
raise RuntimeError("New link key table is larger than the current one")
451438

439+
addr_mgr_entries = []
440+
441+
for dev in devices:
442+
entry = t.AddrMgrEntry(
443+
type=t.AddrMgrUserType.Default,
444+
nwkAddr=dev.node_info.nwk,
445+
extAddr=dev.node_info.ieee,
446+
)
447+
448+
if dev.key is not None:
449+
entry.type |= t.AddrMgrUserType.Security
450+
451+
if dev.is_child:
452+
entry.type |= t.AddrMgrUserType.Assoc
453+
454+
addr_mgr_entries.append(entry)
455+
452456
# Postpone writes until all of the table entries have been created
453-
await write_addr_manager_entries(znp, devices)
457+
await write_addr_manager_entries(znp, addr_mgr_entries)
454458

455459
if old_link_key_table is None:
456460
return

0 commit comments

Comments
 (0)