Skip to content

Commit b3fd089

Browse files
agnersmarcelveldt
andauthored
Shutdown subscription after exactly 30 minutes (#876)
Co-authored-by: Marcel van der Veldt <marcel.vanderveldt@nabucasa.com>
1 parent 5c130ec commit b3fd089

File tree

1 file changed

+28
-21
lines changed

1 file changed

+28
-21
lines changed

matter_server/server/device_controller.py

+28-21
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from chip.clusters.ClusterObjects import ALL_ATTRIBUTES, ALL_CLUSTERS, Cluster
2424
from chip.discovery import DiscoveryType
2525
from chip.exceptions import ChipStackError
26+
from chip.native import PyChipError
2627
from zeroconf import BadTypeInNameException, IPVersion, ServiceStateChange, Zeroconf
2728
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
2829

@@ -80,8 +81,8 @@
8081
NODE_SUBSCRIPTION_CEILING_WIFI = 60
8182
NODE_SUBSCRIPTION_CEILING_THREAD = 60
8283
NODE_SUBSCRIPTION_CEILING_BATTERY_POWERED = 600
83-
NODE_RESUBSCRIBE_ATTEMPTS_UNAVAILABLE = 3
84-
NODE_RESUBSCRIBE_TIMEOUT_OFFLINE = 30 * 60 * 1000
84+
NODE_RESUBSCRIBE_ATTEMPTS_UNAVAILABLE = 2
85+
NODE_RESUBSCRIBE_TIMEOUT_OFFLINE = 30 * 60
8586
NODE_RESUBSCRIBE_FORCE_TIMEOUT = 5
8687
NODE_PING_TIMEOUT = 10
8788
NODE_PING_TIMEOUT_BATTERY_POWERED = 60
@@ -149,7 +150,8 @@ def __init__(
149150
self._node_last_seen_on_mdns: dict[int, float] = {}
150151
self._nodes: dict[int, MatterNodeData] = {}
151152
self._last_known_ip_addresses: dict[int, list[str]] = {}
152-
self._last_subscription_attempt: dict[int, int] = {}
153+
self._resubscription_attempt: dict[int, int] = {}
154+
self._first_resubscribe_attempt: dict[int, float] = {}
153155
self._known_commissioning_params: dict[int, CommissioningParameters] = {}
154156
self._known_commissioning_params_timers: dict[int, asyncio.TimerHandle] = {}
155157
self._aiobrowser: AsyncServiceBrowser | None = None
@@ -1188,29 +1190,37 @@ def resubscription_attempted(
11881190
nextResubscribeIntervalMsec: int,
11891191
) -> None:
11901192
# pylint: disable=unused-argument, invalid-name
1193+
resubscription_attempt = self._resubscription_attempt[node_id]
11911194
node_logger.info(
1192-
"Previous subscription failed with Error: %s, re-subscribing in %s ms...",
1193-
terminationError,
1194-
nextResubscribeIntervalMsec,
1195+
"Subscription failed with %s, resubscription attempt %s",
1196+
str(PyChipError(code=terminationError)),
1197+
resubscription_attempt,
11951198
)
1196-
resubscription_attempt = self._last_subscription_attempt[node_id] + 1
1197-
self._last_subscription_attempt[node_id] = resubscription_attempt
1199+
self._resubscription_attempt[node_id] = resubscription_attempt + 1
1200+
if resubscription_attempt == 0:
1201+
self._first_resubscribe_attempt[node_id] = time.time()
11981202
# Mark node as unavailable and signal consumers.
11991203
# We debounce it a bit so we only mark the node unavailable
1200-
# after some resubscription attempts and we shutdown the subscription
1201-
# if the resubscription interval exceeds 30 minutes (TTL of mdns).
1202-
# The node will be auto picked up by mdns if it's alive again.
1204+
# after some resubscription attempts.
12031205
if resubscription_attempt >= NODE_RESUBSCRIBE_ATTEMPTS_UNAVAILABLE:
12041206
self._node_unavailable(node_id)
1205-
if nextResubscribeIntervalMsec > NODE_RESUBSCRIBE_TIMEOUT_OFFLINE:
1207+
# Shutdown the subscription if we tried to resubscribe for more than 30
1208+
# minutes (typical TTL of mDNS). We assume this device got powered off.
1209+
# When the device gets powered on again, it typically announces itself via
1210+
# mDNS again. The mDNS browsing code will setup the subscription again.
1211+
if (
1212+
time.time() - self._first_resubscribe_attempt[node_id]
1213+
> NODE_RESUBSCRIBE_TIMEOUT_OFFLINE
1214+
):
12061215
asyncio.create_task(self._node_offline(node_id))
12071216

12081217
def resubscription_succeeded(
12091218
transaction: Attribute.SubscriptionTransaction,
12101219
) -> None:
12111220
# pylint: disable=unused-argument, invalid-name
12121221
node_logger.info("Re-Subscription succeeded")
1213-
self._last_subscription_attempt[node_id] = 0
1222+
self._resubscription_attempt[node_id] = 0
1223+
self._first_resubscribe_attempt.pop(node_id, None)
12141224
# mark node as available and signal consumers
12151225
node = self._nodes[node_id]
12161226
if not node.available:
@@ -1233,7 +1243,7 @@ def resubscription_succeeded(
12331243
interval_ceiling = NODE_SUBSCRIPTION_CEILING_BATTERY_POWERED
12341244
else:
12351245
interval_ceiling = NODE_SUBSCRIPTION_CEILING_THREAD
1236-
self._last_subscription_attempt[node_id] = 0
1246+
self._resubscription_attempt[node_id] = 0
12371247
# set-up the actual subscription
12381248
sub: Attribute.SubscriptionTransaction = (
12391249
await self._chip_device_controller.read_attribute(
@@ -1606,15 +1616,12 @@ def _node_unavailable(
16061616
async def _node_offline(self, node_id: int) -> None:
16071617
"""Mark node as offline."""
16081618
# shutdown existing subscriptions
1619+
node_logger = self.get_node_logger(LOGGER, node_id)
1620+
node_logger.info("Node considered offline, shutdown subscription")
16091621
await self._chip_device_controller.shutdown_subscription(node_id)
1622+
16101623
# mark node as unavailable (if it wasn't already)
1611-
node = self._nodes[node_id]
1612-
if not node.available:
1613-
return # nothing to do to
1614-
node.available = False
1615-
self.server.signal_event(EventType.NODE_UPDATED, node)
1616-
node_logger = self.get_node_logger(LOGGER, node_id)
1617-
node_logger.info("Marked node as offline")
1624+
self._node_unavailable(node_id)
16181625

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

0 commit comments

Comments
 (0)