Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion bellows/ezsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from bellows.exception import EzspError, InvalidCommandError, InvalidCommandPayload
from bellows.ezsp import xncp
from bellows.ezsp.config import DEFAULT_CONFIG, RuntimeConfig, ValueConfig
from bellows.ezsp.xncp import FirmwareFeatures, FlowControlType
from bellows.ezsp.xncp import FirmwareFeatures, FlowControlType, GetRouteTableEntryRsp
import bellows.types as t
import bellows.uart

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

# Usually 262144 bytes for MG24
return 32

async def xncp_get_route_table_entry(
self, index: t.uint8_t
) -> GetRouteTableEntryRsp:
"""Get a route table entry."""
return await self.send_xncp_frame(xncp.GetRouteTableEntryReq(index=index))

async def xncp_set_route_table_entry(
self,
index: t.uint8_t,
destination: t.NWK,
next_hop: t.NWK,
status: t.RouteRecordStatus,
cost: t.uint8_t,
) -> None:
"""Set a route table entry."""
await self.send_xncp_frame(
xncp.SetRouteTableEntryReq(
index=index,
destination=destination,
next_hop=next_hop,
status=status,
cost=cost,
)
)
36 changes: 35 additions & 1 deletion bellows/ezsp/xncp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import zigpy.types as t

from bellows.types import EmberStatus, EzspMfgTokenId
from bellows.types import EmberStatus, EzspMfgTokenId, RouteRecordStatus

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -40,13 +40,17 @@ class XncpCommandId(t.enum16):
GET_BUILD_STRING_REQ = 0x0003
GET_FLOW_CONTROL_TYPE_REQ = 0x0004
GET_CHIP_INFO_REQ = 0x0005
SET_ROUTE_TABLE_ENTRY_REQ = 0x0006
GET_ROUTE_TABLE_ENTRY_REQ = 0x0007

GET_SUPPORTED_FEATURES_RSP = GET_SUPPORTED_FEATURES_REQ | 0x8000
SET_SOURCE_ROUTE_RSP = SET_SOURCE_ROUTE_REQ | 0x8000
GET_MFG_TOKEN_OVERRIDE_RSP = GET_MFG_TOKEN_OVERRIDE_REQ | 0x8000
GET_BUILD_STRING_RSP = GET_BUILD_STRING_REQ | 0x8000
GET_FLOW_CONTROL_TYPE_RSP = GET_FLOW_CONTROL_TYPE_REQ | 0x8000
GET_CHIP_INFO_RSP = GET_CHIP_INFO_REQ | 0x8000
SET_ROUTE_TABLE_ENTRY_RSP = SET_ROUTE_TABLE_ENTRY_REQ | 0x8000
GET_ROUTE_TABLE_ENTRY_RSP = GET_ROUTE_TABLE_ENTRY_REQ | 0x8000

UNKNOWN = 0xFFFF

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

# Route table entries can be set
RESTORE_ROUTE_TABLE = 1 << 6


class XncpCommandPayload(t.Struct):
pass
Expand Down Expand Up @@ -183,6 +190,33 @@ class GetChipInfoRsp(XncpCommandPayload):
part_number: t.CharacterString


@register_command(XncpCommandId.SET_ROUTE_TABLE_ENTRY_REQ)
class SetRouteTableEntryReq(XncpCommandPayload):
index: t.uint8_t
destination: t.NWK
next_hop: t.NWK
status: RouteRecordStatus
cost: t.uint8_t


@register_command(XncpCommandId.SET_ROUTE_TABLE_ENTRY_RSP)
class SetRouteTableEntryRsp(XncpCommandPayload):
pass


@register_command(XncpCommandId.GET_ROUTE_TABLE_ENTRY_REQ)
class GetRouteTableEntryReq(XncpCommandPayload):
index: t.uint8_t


@register_command(XncpCommandId.GET_ROUTE_TABLE_ENTRY_RSP)
class GetRouteTableEntryRsp(XncpCommandPayload):
destination: t.NWK
next_hop: t.NWK
status: RouteRecordStatus
cost: t.uint8_t


@register_command(XncpCommandId.UNKNOWN)
class Unknown(XncpCommandPayload):
pass
28 changes: 28 additions & 0 deletions bellows/types/named.py
Original file line number Diff line number Diff line change
Expand Up @@ -2795,3 +2795,31 @@ class SourceRouteDiscoveryMode(basic.enum8):
OFF = 0
ON = 1
RESCHEDULE = 2


class RouteRecordState(basic.enum8):
"""Route record state for EmberRouteTableEntry."""

NO_LONGER_NEEDED = 0
SENT = 1
NEEDED = 2


class RouteRecordStatus(basic.enum8):
"""Route record status for EmberRouteTableEntry."""

ACTIVE_AGE_0 = 0x00
ACTIVE_AGE_1 = 0x40
ACTIVE_AGE_2 = 0x80

BEING_DISCOVERED = 0x01
UNUSED = 0x03
VALIDATING = 0x04


class RouteRecordConcentratortype(basic.enum8):
"""Route record concentrator type for EmberRouteTableEntry."""

NOT_A_CONCENTRATOR = 0
LOW_RAM = 1
HIGH_RAM = 2
8 changes: 4 additions & 4 deletions bellows/types/struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,20 +169,20 @@ class EmberRouteTableEntry(EzspStruct):
# entry is unused.
destination: named.EmberNodeId
# The short id of the next hop to this destination.
nextHop: basic.uint16_t
nextHop: named.EmberNodeId
# Indicates whether this entry is active (0), being discovered (1)),
# unused (3), or validating (4).
status: basic.uint8_t
status: named.RouteRecordStatus
# The number of seconds since this route entry was last used to send a
# packet.
age: basic.uint8_t
# Indicates whether this destination is a High RAM Concentrator (2), a
# Low RAM Concentrator (1), or not a concentrator (0).
concentratorType: basic.uint8_t
concentratorType: named.RouteRecordConcentratortype
# For a High RAM Concentrator, indicates whether a route record is
# needed (2), has been sent (1), or is no long needed (0) because a
# source routed message from the concentrator has been received.
routeRecordState: basic.uint8_t
routeRecordState: named.RouteRecordState


class EmberInitialSecurityState(EzspStruct):
Expand Down
60 changes: 58 additions & 2 deletions bellows/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@
CONF_USE_THREAD,
CONFIG_SCHEMA,
)
from bellows.exception import ControllerError, EzspError, StackAlreadyRunning
from bellows.exception import (
ControllerError,
EzspError,
InvalidCommandError,
StackAlreadyRunning,
)
import bellows.ezsp
from bellows.ezsp.xncp import FirmwareFeatures
import bellows.multicast
Expand Down Expand Up @@ -259,6 +264,29 @@ async def start_network(self):
LOGGER.debug("Setting adapter concurrency to %d", max_concurrent_requests)
self._concurrent_requests_semaphore.max_concurrency = max_concurrent_requests

backup = self.backups.most_recent_backup()
if backup is not None:
await self._restore_route_table(backup.network_info.route_table)

async def _restore_route_table(self, route_table: dict[t.NWK, t.NWK]) -> None:
if FirmwareFeatures.RESTORE_ROUTE_TABLE not in self._ezsp._xncp_features:
LOGGER.debug(
"Firmware does not support writing route table, cannot restore"
)
return

LOGGER.debug("Restoring route table: %s", route_table)

for index, (dest, next_hop) in enumerate(route_table.items()):
# We unconditionally restore route table entries
await self._ezsp.xncp_set_route_table_entry(
index=index,
destination=dest,
next_hop=next_hop,
status=t.RouteRecordStatus.ACTIVE_AGE_2,
cost=0, # unused
)

async def load_network_info(self, *, load_devices=False) -> None:
ezsp = self._ezsp

Expand Down Expand Up @@ -336,6 +364,7 @@ async def load_network_info(self, *, load_devices=False) -> None:
key_table=[],
children=[],
nwk_addresses={},
tx_power=nwk_params.radioTxPower,
stack_specific=stack_specific,
metadata={
"ezsp": {
Expand Down Expand Up @@ -369,6 +398,31 @@ async def load_network_info(self, *, load_devices=False) -> None:
async for nwk, eui64 in ezsp.read_address_table():
self.state.network_info.nwk_addresses[eui64] = nwk

if FirmwareFeatures.RESTORE_ROUTE_TABLE in ezsp._xncp_features:
(status, route_table_size) = await ezsp.getConfigurationValue(
t.EzspConfigId.CONFIG_ROUTE_TABLE_SIZE
)

for index in range(route_table_size):
try:
rsp = await ezsp.xncp_get_route_table_entry(index=index)
except InvalidCommandError:
break

if (
rsp.status
not in (
t.RouteRecordStatus.ACTIVE_AGE_0,
t.RouteRecordStatus.ACTIVE_AGE_1,
t.RouteRecordStatus.ACTIVE_AGE_2,
)
or rsp.destination == 0xFFFF
or rsp.next_hop == 0xFFFF
):
continue

self.state.network_info.route_table[rsp.destination] = rsp.next_hop

async def can_write_network_settings(
self,
*,
Expand Down Expand Up @@ -468,7 +522,7 @@ async def write_network_info(
parameters = t.EmberNetworkParameters()
parameters.panId = t.EmberPanId(network_info.pan_id)
parameters.extendedPanId = t.EUI64(network_info.extended_pan_id)
parameters.radioTxPower = t.uint8_t(8)
parameters.radioTxPower = t.uint8_t(network_info.tx_power)
parameters.radioChannel = t.uint8_t(network_info.channel)
parameters.joinMethod = t.EmberJoinMethod.USE_MAC_ASSOCIATION
parameters.nwkManagerId = t.EmberNodeId(network_info.nwk_manager_id)
Expand All @@ -495,6 +549,8 @@ async def write_network_info(

await self._ensure_network_running()

await self._restore_route_table(network_info.route_table)

async def reset_network_info(self):
await self._ezsp.factory_reset()

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies = [
"click",
"click-log>=0.2.1",
"voluptuous",
"zigpy>=0.83.0",
"zigpy>=0.85.0",
]

[tool.setuptools.packages.find]
Expand Down Expand Up @@ -81,4 +81,4 @@ exclude_also = [
"if TYPE_CHECKING",
"if typing.TYPE_CHECKING",
"@(abc\\.)?abstractmethod",
]
]
Loading
Loading