10
10
from functools import partial
11
11
import logging
12
12
import time
13
- from typing import TYPE_CHECKING , Any , Awaitable , Callable , Iterable , TypeVar , cast
13
+ from typing import TYPE_CHECKING , Any , Callable , Iterable , TypeVar , cast
14
14
15
15
from chip .ChipDeviceCtrl import DeviceProxyWrapper
16
16
from chip .clusters import Attribute , Objects as Clusters
20
20
from zeroconf import IPVersion , ServiceStateChange , Zeroconf
21
21
from zeroconf .asyncio import AsyncServiceBrowser , AsyncServiceInfo , AsyncZeroconf
22
22
23
- from matter_server .common .helpers .util import convert_ip_address
24
23
from matter_server .common .models import CommissionableNodeData , CommissioningParameters
25
24
from matter_server .server .helpers .attributes import parse_attributes_from_read_result
26
25
from matter_server .server .helpers .utils import ping_ip
38
37
dataclass_from_dict ,
39
38
dataclass_to_dict ,
40
39
parse_attribute_path ,
41
- parse_value ,
42
40
)
43
41
from ..common .models import (
44
42
APICommand ,
@@ -97,6 +95,7 @@ def __init__(
97
95
self ._nodes_in_setup : set [int ] = set ()
98
96
self ._mdns_last_seen : dict [int , float ] = {}
99
97
self ._nodes : dict [int , MatterNodeData ] = {}
98
+ self ._last_known_ip_addresses : dict [int , list [str ]] = {}
100
99
self ._last_subscription_attempt : dict [int , int ] = {}
101
100
self .wifi_credentials_set : bool = False
102
101
self .thread_credentials_set : bool = False
@@ -679,14 +678,12 @@ async def subscribe_attribute(
679
678
async def ping_node (self , node_id : int ) -> NodePingResult :
680
679
"""Ping node on the currently known IP-adress(es)."""
681
680
result : NodePingResult = {}
682
- # the node's ip addresses are stored in the GeneralDiagnostics cluster
683
- attribute = Clusters .GeneralDiagnostics .Attributes .NetworkInterfaces
684
- attr_path = f"0/{ attribute .cluster_id } /{ attribute .attribute_id } "
685
681
node = self ._nodes .get (node_id )
686
682
if node is None :
687
683
raise NodeNotExists (
688
684
f"Node { node_id } does not exist or is not yet interviewed"
689
685
)
686
+ node_logger = LOGGER .getChild (f"[node { node_id } ]" )
690
687
691
688
battery_powered = (
692
689
node .attributes .get (ROUTING_ROLE_ATTRIBUTE_PATH , 0 )
@@ -696,41 +693,71 @@ async def ping_node(self, node_id: int) -> NodePingResult:
696
693
async def _do_ping (ip_address : str ) -> None :
697
694
"""Ping IP and add to result."""
698
695
timeout = 10 if battery_powered else 2
699
- result [ip_address ] = await ping_ip (ip_address , timeout )
700
-
701
- # The network interfaces attribute contains a list of network interfaces.
702
- # For regular nodes this is just a single interface but we iterate them all anyway.
703
- # Create a list of tasks so we can do multiple pings simultanuous.
704
- # NOTE: Upgrade this to a TaskGroup once we bump our minimal python version.
705
- attr_data = cast (list [dict [str , Any ]], node .attributes .get (attr_path ))
706
- tasks : list [Awaitable ] = []
707
- for network_interface_data in attr_data :
708
- network_interface : Clusters .GeneralDiagnostics .Structs .NetworkInterface = (
709
- parse_value (
710
- "network_interface" ,
711
- network_interface_data ,
712
- Clusters .GeneralDiagnostics .Structs .NetworkInterface ,
696
+ if "%" in ip_address :
697
+ # ip address contains an interface index
698
+ clean_ip , interface_idx = ip_address .split ("%" , 1 )
699
+ node_logger .debug (
700
+ "Pinging address %s (using interface %s)" , clean_ip , interface_idx
713
701
)
714
- )
715
- # ignore invalid/non-operational interfaces
716
- if not network_interface .isOperational :
717
- continue
718
- if network_interface .type in (
719
- Clusters .GeneralDiagnostics .Enums .InterfaceTypeEnum .kUnspecified ,
720
- Clusters .GeneralDiagnostics .Enums .InterfaceTypeEnum .kUnknownEnumValue ,
721
- ):
722
- continue
702
+ else :
703
+ clean_ip = ip_address
704
+ node_logger .debug ("Pinging address %s" , clean_ip )
705
+ result [clean_ip ] = await ping_ip (ip_address , timeout )
723
706
724
- # enumerate ipv4 and ipv6 addresses
725
- for ipv4_address_hex in network_interface .IPv4Addresses :
726
- ipv4_address = convert_ip_address (ipv4_address_hex )
727
- tasks .append (_do_ping (ipv4_address ))
728
- for ipv6_address_hex in network_interface .IPv6Addresses :
729
- ipv6_address = convert_ip_address (ipv6_address_hex , True )
730
- tasks .append (_do_ping (ipv6_address ))
707
+ ip_addresses = await self .get_node_ip_addresses (
708
+ node_id , prefer_cache = False , scoped = True
709
+ )
710
+ tasks = [_do_ping (x ) for x in ip_addresses ]
711
+ # TODO: replace this gather with a taskgroup once we bump our py version
731
712
await asyncio .gather (* tasks )
713
+
714
+ # retrieve the currently connected/used address which is used
715
+ # by the sdk for communicating with the device
716
+ if TYPE_CHECKING :
717
+ assert self .chip_controller is not None
718
+ if sdk_result := await self ._call_sdk (
719
+ self .chip_controller .GetAddressAndPort , nodeid = node_id
720
+ ):
721
+ active_address = sdk_result [0 ]
722
+ node_logger .info (
723
+ "The SDK is communicating with the device using %s" , active_address
724
+ )
725
+ if active_address not in result and node .available :
726
+ # if the sdk is connected to a node, treat the address as pingable
727
+ result [active_address ] = True
728
+
732
729
return result
733
730
731
+ @api_command (APICommand .GET_NODE_IP_ADRESSES )
732
+ async def get_node_ip_addresses (
733
+ self , node_id : int , prefer_cache : bool = False , scoped : bool = False
734
+ ) -> list [str ]:
735
+ """Return the currently known (scoped) IP-adress(es)."""
736
+ cached_info = self ._last_known_ip_addresses .get (node_id , [])
737
+ if prefer_cache and cached_info :
738
+ return cached_info if scoped else [x .split ("%" )[0 ] for x in cached_info ]
739
+ node = self ._nodes .get (node_id )
740
+ if node is None :
741
+ raise NodeNotExists (
742
+ f"Node { node_id } does not exist or is not yet interviewed"
743
+ )
744
+ node_logger = LOGGER .getChild (f"[node { node_id } ]" )
745
+ # query mdns for all IP's
746
+ # ensure both fabric id and node id have 16 characters (prefix with zero's)
747
+ mdns_name = f"{ self .compressed_fabric_id :0{16 }X} -{ node_id :0{16 }X} .{ MDNS_TYPE_OPERATIONAL_NODE } "
748
+ info = AsyncServiceInfo (MDNS_TYPE_OPERATIONAL_NODE , mdns_name )
749
+ if TYPE_CHECKING :
750
+ assert self ._aiozc is not None
751
+ if not await info .async_request (self ._aiozc .zeroconf , 3000 ):
752
+ node_logger .info (
753
+ "Node could not be discovered on the network, returning cached IP's"
754
+ )
755
+ return cached_info
756
+ ip_adresses = info .parsed_scoped_addresses (IPVersion .All )
757
+ # cache this info for later use
758
+ self ._last_known_ip_addresses [node_id ] = ip_adresses
759
+ return ip_adresses if scoped else [x .split ("%" )[0 ] for x in ip_adresses ]
760
+
734
761
async def _subscribe_node (self , node_id : int ) -> None :
735
762
"""
736
763
Subscribe to all node state changes/events for an individual node.
@@ -989,6 +1016,8 @@ async def _setup_node(self, node_id: int) -> None:
989
1016
# prevent duplicate setup actions
990
1017
return
991
1018
self ._nodes_in_setup .add (node_id )
1019
+ # pre-cache ip-addresses
1020
+ await self .get_node_ip_addresses (node_id )
992
1021
try :
993
1022
# (re)interview node (only) if needed
994
1023
node_data = self ._nodes [node_id ]
0 commit comments