Skip to content

Commit 0ef533b

Browse files
authored
Merge branch 'master' into feature/message-cluster-present-message-update
2 parents 0578255 + b1bf11e commit 0ef533b

File tree

4 files changed

+291
-0
lines changed

4 files changed

+291
-0
lines changed

.github/.wordlist.txt

+1
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ GPL
634634
GPLv
635635
Gradle
636636
gradlew
637+
graphviz
637638
Groupcast
638639
GroupId
639640
GroupKeyManagement

src/tools/device-graph/README.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
orphan: true
3+
---
4+
5+
# Setup
6+
7+
This tool uses the python environment used by the python_testing efforts, which
8+
can be built using the below command. Notice that graphviz is required as an
9+
extra package in order for the tool to generate the graph file.
10+
11+
```
12+
scripts/build_python.sh -m platform -i out/python_env --extra_packages graphviz
13+
```
14+
15+
Once the python environment is build it can be activated using this command:
16+
17+
```
18+
source out/python_env/bin/activate
19+
```
20+
21+
# How to run
22+
23+
When the python environment is activated the tool can be started as a regular
24+
python script. The tool does rely on the "framework" used for python testing,
25+
which means it is possible to do the commissioning of the DUT as well.
26+
27+
By adding the appropriate parameters to the script execution, it will
28+
automatically perform a commissioning before running the tool itself.
29+
30+
This is an example of running the test including commissioning a Thread based
31+
example app device
32+
33+
```
34+
python3 '/Users/renejosefsen/Developer/GitData/connectedhomeip/src/tools/device-graph/matter-device-graph.py' --commissioning-method ble-thread --discriminator 3840 --passcode 20202021 --thread-dataset-hex 0e08000000000001000035060004001fffe00708fdbeb88eb19ecbe60410ec73aeaadc21448df01599e6eaf216eb0c0402a0f7f8000300001901025b3502085b35dead5b35beef030435623335051000112233445566778899aabbccddeeff
35+
```
36+
37+
In case the setup code and discriminator is not available, the QR code can also
38+
be used:
39+
40+
```
41+
python3 '/Users/renejosefsen/Developer/GitData/connectedhomeip/src/tools/device-graph/matter-device-graph.py' --commissioning-method ble-thread --qr-code MT:K2AA04EG15LL6I0LF00 --thread-dataset-hex 0e08000000000001000035060004001fffe00708fd6df9cc6d0db45b0410e12c1d624d8b4daf6adbfe5b2cd7787b0c0402a0f7f8000300001901025b3502085b35dead5b35beef030435623335051000112233445566778899aabbccddeeff
42+
```
43+
44+
In case the device uses a development PAA, the following parameter should be
45+
added.
46+
47+
```
48+
--paa-trust-store-path credentials/development/paa-root-certs
49+
```
50+
51+
In case the device uses a production PAA, the following parameter should be
52+
added.
53+
54+
```
55+
--paa-trust-store-path credentials/production/paa-root-certs
56+
```
57+
58+
Once a commissioning is completed for the device, is is possible to rerun the
59+
tool again for an already commissioned devices, this is an example of how to do
60+
so:
61+
62+
```
63+
python3 '/Users/renejosefsen/Developer/GitData/connectedhomeip/src/tools/device-graph/matter-device-graph.py'
64+
```
65+
66+
The tool currently outputs the dot file in this folder and the output file is
67+
named "matter-device-graph.dot".
68+
69+
# How to view graph
70+
71+
In order to view the graph, any tool that renders dot/graphviz files can be
72+
used.
73+
74+
It is possible to open dot files and get them rendered in vscode using this
75+
extension:
76+
[vscode-graphviz](https://marketplace.visualstudio.com/items?itemName=joaompinto.vscode-graphviz)
77+
78+
# Example of output
79+
80+
This is an example of the graph outputted from a device:
81+
82+
![matter device graph example](./matter-device-graph-example.png)
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#
2+
# Copyright (c) 2023 Project CHIP Authors
3+
# All rights reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
import os
19+
import pprint
20+
import sys
21+
22+
import chip.clusters as Clusters
23+
import graphviz
24+
from rich.console import Console
25+
26+
# Add the path to python_testing folder, in order to be able to import from matter_testing_support
27+
sys.path.append(os.path.abspath(sys.path[0] + "/../../python_testing"))
28+
from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main # noqa: E402
29+
30+
console = None
31+
maxClusterNameLength = 30
32+
33+
34+
# Given there is currently no tranlation from DeviceTypeID to the device type name,
35+
# this dict is created for now. When some more general is available, it should be updated to use this.
36+
deviceTypeDict = {
37+
22: "Root Node",
38+
17: "Power Source",
39+
18: "OTA Requestor",
40+
20: "OTA Provider",
41+
14: "Aggregator",
42+
19: "Bridged Node",
43+
256: "On/Off Light",
44+
257: "Dimmable Light",
45+
268: "Color Temperature Light",
46+
269: "Extended Color Light",
47+
266: "On/Off Plug-in Unit",
48+
267: "Dimmable Plug-In Unit",
49+
771: "Pump",
50+
259: "On/Off Light Switch",
51+
260: "Dimmer Switch",
52+
261: "Color Dimmer Switch",
53+
2112: "Control Bridge",
54+
772: "Pump Controller",
55+
15: "Generic Switch",
56+
21: "Contact Sensor",
57+
262: "Light Sensor",
58+
263: "Occupancy Sensor",
59+
770: "Temperature Sensor",
60+
773: "Pressure Sensor",
61+
774: "Flow Sensor",
62+
775: "Humidity Sensor",
63+
2128: "On/Off Sensor",
64+
10: "Door Lock",
65+
11: "Door Lock Controller",
66+
514: "Window Covering",
67+
515: "Window Covering Controller",
68+
768: "Heating/Cooling Unit",
69+
769: "Thermostat",
70+
43: "Fan",
71+
35: "Casting Video Player",
72+
34: "Speaker",
73+
36: "Content App",
74+
40: "Basic Video Player",
75+
41: "Casting Video Client",
76+
42: "Video Remote Control",
77+
39: "Mode Select",
78+
45: "Air Purifier",
79+
44: "Air Quality Sensor",
80+
112: "Refrigerator",
81+
113: "Temperature Controlled Cabinet",
82+
114: "Room Air Conditioner",
83+
115: "Laundry Washer",
84+
116: "Robotic Vacuum Cleaner",
85+
117: "Dishwasher",
86+
118: "Smoke CO Alarm"
87+
}
88+
89+
90+
def AddServerOrClientNode(graphSection, endpoint, clusterName, color, nodeRef):
91+
92+
if (len(clusterName) > maxClusterNameLength):
93+
clusterNameAdjustedLength = clusterName[:maxClusterNameLength] + '...'
94+
else:
95+
clusterNameAdjustedLength = clusterName
96+
97+
graphSection.node(f"ep{endpoint}_{clusterName}", label=f"{clusterNameAdjustedLength}", style="filled,rounded",
98+
color=color, shape="box", fixedsize="true", width="3", height="0.5")
99+
graphSection.edge(nodeRef, f"ep{endpoint}_{clusterName}", style="invis")
100+
101+
102+
def CreateEndpointGraph(graph, graphSection, endpoint, wildcardResponse):
103+
104+
numberOfRowsInEndpoint = 2
105+
106+
partsListFromWildcardRead = wildcardResponse[endpoint][Clusters.Objects.Descriptor][Clusters.Objects.Descriptor.Attributes.PartsList]
107+
108+
listOfDeviceTypes = []
109+
for deviceTypeStruct in wildcardResponse[endpoint][Clusters.Objects.Descriptor][Clusters.Objects.Descriptor.Attributes.DeviceTypeList]:
110+
try:
111+
listOfDeviceTypes.append(deviceTypeDict[deviceTypeStruct.deviceType])
112+
except KeyError:
113+
listOfDeviceTypes.append(deviceTypeStruct.deviceType)
114+
115+
# console.print(f"Endpoint: {endpoint}")
116+
# console.print(f"DeviceTypeList: {listOfDeviceTypes}")
117+
# console.print(f"PartsList: {partsListFromWildcardRead}")
118+
119+
endpointLabel = f"Endpoint: {endpoint}\lDeviceTypeList: {listOfDeviceTypes}\lPartsList: {partsListFromWildcardRead}\l" # noqa: W605
120+
121+
nextNodeRef = ""
122+
nodeRef = f"ep{endpoint}"
123+
clusterColumnCount = 0
124+
125+
graphSection.node(f"ep{endpoint}", label=endpointLabel, style="filled,rounded",
126+
color="dodgerblue", shape="box", fixedsize="true", width="4", height="1")
127+
128+
for clusterId in wildcardResponse[endpoint][Clusters.Objects.Descriptor][Clusters.Objects.Descriptor.Attributes.ServerList]:
129+
clusterColumnCount += 1
130+
131+
try:
132+
clusterName = Clusters.ClusterObjects.ALL_CLUSTERS[clusterId].__name__
133+
except KeyError:
134+
clusterName = f"Custom server\l0x{clusterId:08X}" # noqa: W605
135+
136+
AddServerOrClientNode(graphSection, endpoint, clusterName, "olivedrab", nodeRef)
137+
138+
if clusterColumnCount == 2:
139+
nextNodeRef = f"ep{endpoint}_{clusterName}"
140+
elif clusterColumnCount == 3:
141+
nodeRef = nextNodeRef
142+
clusterColumnCount = 0
143+
numberOfRowsInEndpoint += 1
144+
145+
for clusterId in wildcardResponse[endpoint][Clusters.Objects.Descriptor][Clusters.Objects.Descriptor.Attributes.ClientList]:
146+
clusterColumnCount += 1
147+
148+
try:
149+
clusterName = Clusters.ClusterObjects.ALL_CLUSTERS[clusterId].__name__
150+
except KeyError:
151+
clusterName = f"Custom client\l0x{clusterId:08X}" # noqa: W605
152+
153+
AddServerOrClientNode(graphSection, endpoint, clusterName, "orange", nodeRef)
154+
155+
if clusterColumnCount == 2:
156+
nextNodeRef = f"ep{endpoint}_{clusterName}"
157+
elif clusterColumnCount == 3:
158+
nodeRef = nextNodeRef
159+
clusterColumnCount = 0
160+
numberOfRowsInEndpoint += 1
161+
162+
if endpoint != 0:
163+
# Create link to endpoints in the parts list
164+
for part in partsListFromWildcardRead:
165+
graph.edge(f"ep{endpoint}", f"ep{part}", ltail=f"cluster_{endpoint}", minlen=f"{numberOfRowsInEndpoint}")
166+
167+
168+
class TC_MatterDeviceGraph(MatterBaseTest):
169+
@async_test_body
170+
async def test_matter_device_graph(self):
171+
172+
# Create console to print
173+
global console
174+
console = Console()
175+
176+
# Run descriptor validation test
177+
dev_ctrl = self.default_controller
178+
179+
# Perform wildcard read to get all attributes from device
180+
console.print("[blue]Capturing data from device")
181+
wildcardResponse = await dev_ctrl.ReadAttribute(self.dut_node_id, [('*')])
182+
# console.print(wildcardResponse)
183+
184+
# Creating graph object
185+
deviceGraph = graphviz.Digraph()
186+
deviceGraph.attr(style="rounded", splines="line", compound="true")
187+
188+
console.print("[blue]Generating graph")
189+
# Loop through each endpoint in the response from the wildcard read
190+
for endpoint in wildcardResponse:
191+
192+
if endpoint == 0:
193+
with deviceGraph.subgraph(name='cluster_rootnode') as rootNodeSection:
194+
CreateEndpointGraph(deviceGraph, rootNodeSection, endpoint, wildcardResponse)
195+
else:
196+
with deviceGraph.subgraph(name='cluster_endpoints') as endpointsSection:
197+
with endpointsSection.subgraph(name=f'cluster_{endpoint}') as endpointSection:
198+
CreateEndpointGraph(deviceGraph, endpointSection, endpoint, wildcardResponse)
199+
200+
deviceGraph.save(f'{sys.path[0]}/matter-device-graph.dot')
201+
202+
deviceDataFile = open(f'{sys.path[0]}/matter-device-data.txt', 'w')
203+
deviceDataFile.write(pprint.pformat((wildcardResponse)))
204+
deviceDataFile.close()
205+
206+
207+
if __name__ == "__main__":
208+
default_matter_test_main()

0 commit comments

Comments
 (0)