Skip to content

Commit b48e669

Browse files
committed
Override Liveness timeout on shutdown
Override the liveness timeout when a node announces a shutdown. This causes the subscription to fail quickly. Set the timeout to 0 soon after. Setting to 0 causes the SDK to use the default liveness timeout from the next (successful) subscription onwards again. Another alternative would be to shutdown the subscription (mark the device as offline). This has the disadvantage that the device needs to recreate the subscription from scratch when it comes back, which causes unnecessary traffic. If the node stays offline for longer than 30s, we'll still trigger the offline logic which will shutdown the subscription entirely
1 parent 0eefa43 commit b48e669

File tree

2 files changed

+41
-4
lines changed

2 files changed

+41
-4
lines changed

matter_server/server/device_controller.py

+34-4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
NODE_SUBSCRIPTION_CEILING_BATTERY_POWERED = 600
7878
NODE_RESUBSCRIBE_ATTEMPTS_UNAVAILABLE = 3
7979
NODE_RESUBSCRIBE_TIMEOUT_OFFLINE = 30 * 60 * 1000
80+
NODE_RESUBSCRIBE_FORCE_TIMEOUT = 5
8081
NODE_PING_TIMEOUT = 10
8182
NODE_PING_TIMEOUT_BATTERY_POWERED = 60
8283
NODE_MDNS_BACKOFF = 610 # must be higher than (highest) sub ceiling
@@ -1109,7 +1110,10 @@ def event_callback(
11091110
self.event_history.append(node_event)
11101111

11111112
if isinstance(data.Data, Clusters.BasicInformation.Events.ShutDown):
1112-
self._node_unavailable(node_id)
1113+
# Force resubscription after a shutdown event. Otherwise we'd have to
1114+
# wait for up to NODE_SUBSCRIPTION_CEILING_BATTERY_POWERED minutes for
1115+
# the SDK to notice the device is gone.
1116+
self._node_unavailable(node_id, True)
11131117

11141118
self.server.signal_event(EventType.NODE_EVENT, node_event)
11151119

@@ -1490,15 +1494,40 @@ def _write_node_state(self, node_id: int, force: bool = False) -> None:
14901494
force=force,
14911495
)
14921496

1493-
def _node_unavailable(self, node_id: int) -> None:
1497+
def _node_unavailable(
1498+
self, node_id: int, force_resubscription: bool = False
1499+
) -> None:
14941500
"""Mark node as unavailable."""
14951501
# mark node as unavailable (if it wasn't already)
14961502
node = self._nodes[node_id]
14971503
if not node.available:
14981504
return
14991505
node.available = False
15001506
self.server.signal_event(EventType.NODE_UPDATED, node)
1501-
LOGGER.info("Marked node %s as unavailable", node_id)
1507+
node_logger = LOGGER.getChild(f"node_{node_id}")
1508+
node_logger.info("Marked node as unavailable")
1509+
if force_resubscription:
1510+
# Make sure the subscriptions are expiring very soon to trigger subscription
1511+
# resumption logic quickly. This is especially important for battery operated
1512+
# devices so subscription resumption logic kicks in quickly.
1513+
node_logger.info(
1514+
"Forcing subscription timeout in %ds", NODE_RESUBSCRIBE_FORCE_TIMEOUT
1515+
)
1516+
asyncio.create_task(
1517+
self._chip_device_controller.subscription_override_liveness_timeout(
1518+
node_id, NODE_RESUBSCRIBE_FORCE_TIMEOUT * 1000
1519+
)
1520+
)
1521+
# Clear the timeout soon after the scheduled timeout above. For future
1522+
# subscriptions we want to use the default timeout again.
1523+
self._loop.call_later(
1524+
NODE_RESUBSCRIBE_FORCE_TIMEOUT + 1,
1525+
lambda: asyncio.create_task(
1526+
self._chip_device_controller.subscription_override_liveness_timeout(
1527+
node_id, 0
1528+
)
1529+
),
1530+
)
15021531

15031532
async def _node_offline(self, node_id: int) -> None:
15041533
"""Mark node as offline."""
@@ -1510,7 +1539,8 @@ async def _node_offline(self, node_id: int) -> None:
15101539
return # nothing to do to
15111540
node.available = False
15121541
self.server.signal_event(EventType.NODE_UPDATED, node)
1513-
LOGGER.info("Marked node %s as offline", node_id)
1542+
node_logger = LOGGER.getChild(f"node_{node_id}")
1543+
node_logger.info("Marked node as offline")
15141544

15151545
async def _fallback_node_scanner(self) -> None:
15161546
"""Scan for operational nodes in the background that are missed by mdns."""

matter_server/server/sdk.py

+7
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ async def shutdown_subscription(self, node_id: int) -> None:
395395
if sub := self._subscriptions.pop(node_id, None):
396396
await self._call_sdk(sub.Shutdown)
397397

398+
async def subscription_override_liveness_timeout(
399+
self, node_id: int, liveness_timeout_ms: int
400+
) -> None:
401+
"""Override the liveness timeout for the subscription of the node."""
402+
if sub := self._subscriptions.get(node_id, None):
403+
await self._call_sdk(sub.OverrideLivenessTimeoutMs, liveness_timeout_ms)
404+
398405
def node_has_subscription(self, node_id: int) -> bool:
399406
"""Check if a node has an active subscription."""
400407
return node_id in self._subscriptions

0 commit comments

Comments
 (0)