Skip to content

Commit 02d7870

Browse files
authored
Backup/restore routing table and allow specifying TX power during formation (#690)
* Restore the routing table on startup * Account for new zigpy changes * Read the route table size * Rename `RouteRecordStatus` constants * Bump zigpy to 0.85.0 * Add unit tests * Re-add accidentally removed code * Clean up
1 parent 4d31bb4 commit 02d7870

File tree

8 files changed

+405
-13
lines changed

8 files changed

+405
-13
lines changed

bellows/ezsp/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from bellows.exception import EzspError, InvalidCommandError, InvalidCommandPayload
2121
from bellows.ezsp import xncp
2222
from bellows.ezsp.config import DEFAULT_CONFIG, RuntimeConfig, ValueConfig
23-
from bellows.ezsp.xncp import FirmwareFeatures, FlowControlType
23+
from bellows.ezsp.xncp import FirmwareFeatures, FlowControlType, GetRouteTableEntryRsp
2424
import bellows.types as t
2525
import bellows.uart
2626

@@ -815,3 +815,28 @@ async def get_default_adapter_concurrency(self) -> int:
815815

816816
# Usually 262144 bytes for MG24
817817
return 32
818+
819+
async def xncp_get_route_table_entry(
820+
self, index: t.uint8_t
821+
) -> GetRouteTableEntryRsp:
822+
"""Get a route table entry."""
823+
return await self.send_xncp_frame(xncp.GetRouteTableEntryReq(index=index))
824+
825+
async def xncp_set_route_table_entry(
826+
self,
827+
index: t.uint8_t,
828+
destination: t.NWK,
829+
next_hop: t.NWK,
830+
status: t.RouteRecordStatus,
831+
cost: t.uint8_t,
832+
) -> None:
833+
"""Set a route table entry."""
834+
await self.send_xncp_frame(
835+
xncp.SetRouteTableEntryReq(
836+
index=index,
837+
destination=destination,
838+
next_hop=next_hop,
839+
status=status,
840+
cost=cost,
841+
)
842+
)

bellows/ezsp/xncp.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import zigpy.types as t
99

10-
from bellows.types import EmberStatus, EzspMfgTokenId
10+
from bellows.types import EmberStatus, EzspMfgTokenId, RouteRecordStatus
1111

1212
_LOGGER = logging.getLogger(__name__)
1313

@@ -40,13 +40,17 @@ class XncpCommandId(t.enum16):
4040
GET_BUILD_STRING_REQ = 0x0003
4141
GET_FLOW_CONTROL_TYPE_REQ = 0x0004
4242
GET_CHIP_INFO_REQ = 0x0005
43+
SET_ROUTE_TABLE_ENTRY_REQ = 0x0006
44+
GET_ROUTE_TABLE_ENTRY_REQ = 0x0007
4345

4446
GET_SUPPORTED_FEATURES_RSP = GET_SUPPORTED_FEATURES_REQ | 0x8000
4547
SET_SOURCE_ROUTE_RSP = SET_SOURCE_ROUTE_REQ | 0x8000
4648
GET_MFG_TOKEN_OVERRIDE_RSP = GET_MFG_TOKEN_OVERRIDE_REQ | 0x8000
4749
GET_BUILD_STRING_RSP = GET_BUILD_STRING_REQ | 0x8000
4850
GET_FLOW_CONTROL_TYPE_RSP = GET_FLOW_CONTROL_TYPE_REQ | 0x8000
4951
GET_CHIP_INFO_RSP = GET_CHIP_INFO_REQ | 0x8000
52+
SET_ROUTE_TABLE_ENTRY_RSP = SET_ROUTE_TABLE_ENTRY_REQ | 0x8000
53+
GET_ROUTE_TABLE_ENTRY_RSP = GET_ROUTE_TABLE_ENTRY_REQ | 0x8000
5054

5155
UNKNOWN = 0xFFFF
5256

@@ -111,6 +115,9 @@ class FirmwareFeatures(t.bitmap32):
111115
# Chip info (e.g. name, RAM size) can be read
112116
CHIP_INFO = 1 << 5
113117

118+
# Route table entries can be set
119+
RESTORE_ROUTE_TABLE = 1 << 6
120+
114121

115122
class XncpCommandPayload(t.Struct):
116123
pass
@@ -183,6 +190,33 @@ class GetChipInfoRsp(XncpCommandPayload):
183190
part_number: t.CharacterString
184191

185192

193+
@register_command(XncpCommandId.SET_ROUTE_TABLE_ENTRY_REQ)
194+
class SetRouteTableEntryReq(XncpCommandPayload):
195+
index: t.uint8_t
196+
destination: t.NWK
197+
next_hop: t.NWK
198+
status: RouteRecordStatus
199+
cost: t.uint8_t
200+
201+
202+
@register_command(XncpCommandId.SET_ROUTE_TABLE_ENTRY_RSP)
203+
class SetRouteTableEntryRsp(XncpCommandPayload):
204+
pass
205+
206+
207+
@register_command(XncpCommandId.GET_ROUTE_TABLE_ENTRY_REQ)
208+
class GetRouteTableEntryReq(XncpCommandPayload):
209+
index: t.uint8_t
210+
211+
212+
@register_command(XncpCommandId.GET_ROUTE_TABLE_ENTRY_RSP)
213+
class GetRouteTableEntryRsp(XncpCommandPayload):
214+
destination: t.NWK
215+
next_hop: t.NWK
216+
status: RouteRecordStatus
217+
cost: t.uint8_t
218+
219+
186220
@register_command(XncpCommandId.UNKNOWN)
187221
class Unknown(XncpCommandPayload):
188222
pass

bellows/types/named.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2795,3 +2795,31 @@ class SourceRouteDiscoveryMode(basic.enum8):
27952795
OFF = 0
27962796
ON = 1
27972797
RESCHEDULE = 2
2798+
2799+
2800+
class RouteRecordState(basic.enum8):
2801+
"""Route record state for EmberRouteTableEntry."""
2802+
2803+
NO_LONGER_NEEDED = 0
2804+
SENT = 1
2805+
NEEDED = 2
2806+
2807+
2808+
class RouteRecordStatus(basic.enum8):
2809+
"""Route record status for EmberRouteTableEntry."""
2810+
2811+
ACTIVE_AGE_0 = 0x00
2812+
ACTIVE_AGE_1 = 0x40
2813+
ACTIVE_AGE_2 = 0x80
2814+
2815+
BEING_DISCOVERED = 0x01
2816+
UNUSED = 0x03
2817+
VALIDATING = 0x04
2818+
2819+
2820+
class RouteRecordConcentratortype(basic.enum8):
2821+
"""Route record concentrator type for EmberRouteTableEntry."""
2822+
2823+
NOT_A_CONCENTRATOR = 0
2824+
LOW_RAM = 1
2825+
HIGH_RAM = 2

bellows/types/struct.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,20 @@ class EmberRouteTableEntry(EzspStruct):
169169
# entry is unused.
170170
destination: named.EmberNodeId
171171
# The short id of the next hop to this destination.
172-
nextHop: basic.uint16_t
172+
nextHop: named.EmberNodeId
173173
# Indicates whether this entry is active (0), being discovered (1)),
174174
# unused (3), or validating (4).
175-
status: basic.uint8_t
175+
status: named.RouteRecordStatus
176176
# The number of seconds since this route entry was last used to send a
177177
# packet.
178178
age: basic.uint8_t
179179
# Indicates whether this destination is a High RAM Concentrator (2), a
180180
# Low RAM Concentrator (1), or not a concentrator (0).
181-
concentratorType: basic.uint8_t
181+
concentratorType: named.RouteRecordConcentratortype
182182
# For a High RAM Concentrator, indicates whether a route record is
183183
# needed (2), has been sent (1), or is no long needed (0) because a
184184
# source routed message from the concentrator has been received.
185-
routeRecordState: basic.uint8_t
185+
routeRecordState: named.RouteRecordState
186186

187187

188188
class EmberInitialSecurityState(EzspStruct):

bellows/zigbee/application.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@
3232
CONF_USE_THREAD,
3333
CONFIG_SCHEMA,
3434
)
35-
from bellows.exception import ControllerError, EzspError, StackAlreadyRunning
35+
from bellows.exception import (
36+
ControllerError,
37+
EzspError,
38+
InvalidCommandError,
39+
StackAlreadyRunning,
40+
)
3641
import bellows.ezsp
3742
from bellows.ezsp.xncp import FirmwareFeatures
3843
import bellows.multicast
@@ -259,6 +264,29 @@ async def start_network(self):
259264
LOGGER.debug("Setting adapter concurrency to %d", max_concurrent_requests)
260265
self._concurrent_requests_semaphore.max_concurrency = max_concurrent_requests
261266

267+
backup = self.backups.most_recent_backup()
268+
if backup is not None:
269+
await self._restore_route_table(backup.network_info.route_table)
270+
271+
async def _restore_route_table(self, route_table: dict[t.NWK, t.NWK]) -> None:
272+
if FirmwareFeatures.RESTORE_ROUTE_TABLE not in self._ezsp._xncp_features:
273+
LOGGER.debug(
274+
"Firmware does not support writing route table, cannot restore"
275+
)
276+
return
277+
278+
LOGGER.debug("Restoring route table: %s", route_table)
279+
280+
for index, (dest, next_hop) in enumerate(route_table.items()):
281+
# We unconditionally restore route table entries
282+
await self._ezsp.xncp_set_route_table_entry(
283+
index=index,
284+
destination=dest,
285+
next_hop=next_hop,
286+
status=t.RouteRecordStatus.ACTIVE_AGE_2,
287+
cost=0, # unused
288+
)
289+
262290
async def load_network_info(self, *, load_devices=False) -> None:
263291
ezsp = self._ezsp
264292

@@ -336,6 +364,7 @@ async def load_network_info(self, *, load_devices=False) -> None:
336364
key_table=[],
337365
children=[],
338366
nwk_addresses={},
367+
tx_power=nwk_params.radioTxPower,
339368
stack_specific=stack_specific,
340369
metadata={
341370
"ezsp": {
@@ -369,6 +398,31 @@ async def load_network_info(self, *, load_devices=False) -> None:
369398
async for nwk, eui64 in ezsp.read_address_table():
370399
self.state.network_info.nwk_addresses[eui64] = nwk
371400

401+
if FirmwareFeatures.RESTORE_ROUTE_TABLE in ezsp._xncp_features:
402+
(status, route_table_size) = await ezsp.getConfigurationValue(
403+
t.EzspConfigId.CONFIG_ROUTE_TABLE_SIZE
404+
)
405+
406+
for index in range(route_table_size):
407+
try:
408+
rsp = await ezsp.xncp_get_route_table_entry(index=index)
409+
except InvalidCommandError:
410+
break
411+
412+
if (
413+
rsp.status
414+
not in (
415+
t.RouteRecordStatus.ACTIVE_AGE_0,
416+
t.RouteRecordStatus.ACTIVE_AGE_1,
417+
t.RouteRecordStatus.ACTIVE_AGE_2,
418+
)
419+
or rsp.destination == 0xFFFF
420+
or rsp.next_hop == 0xFFFF
421+
):
422+
continue
423+
424+
self.state.network_info.route_table[rsp.destination] = rsp.next_hop
425+
372426
async def can_write_network_settings(
373427
self,
374428
*,
@@ -468,7 +522,7 @@ async def write_network_info(
468522
parameters = t.EmberNetworkParameters()
469523
parameters.panId = t.EmberPanId(network_info.pan_id)
470524
parameters.extendedPanId = t.EUI64(network_info.extended_pan_id)
471-
parameters.radioTxPower = t.uint8_t(8)
525+
parameters.radioTxPower = t.uint8_t(network_info.tx_power)
472526
parameters.radioChannel = t.uint8_t(network_info.channel)
473527
parameters.joinMethod = t.EmberJoinMethod.USE_MAC_ASSOCIATION
474528
parameters.nwkManagerId = t.EmberNodeId(network_info.nwk_manager_id)
@@ -495,6 +549,8 @@ async def write_network_info(
495549

496550
await self._ensure_network_running()
497551

552+
await self._restore_route_table(network_info.route_table)
553+
498554
async def reset_network_info(self):
499555
await self._ezsp.factory_reset()
500556

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies = [
1717
"click",
1818
"click-log>=0.2.1",
1919
"voluptuous",
20-
"zigpy>=0.83.0",
20+
"zigpy>=0.85.0",
2121
]
2222

2323
[tool.setuptools.packages.find]
@@ -81,4 +81,4 @@ exclude_also = [
8181
"if TYPE_CHECKING",
8282
"if typing.TYPE_CHECKING",
8383
"@(abc\\.)?abstractmethod",
84-
]
84+
]

0 commit comments

Comments
 (0)