forked from project-chip/connectedhomeip
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial version of matter device graph tool (project-chip#30187)
* Initial commit of matter device graph tool * Fix a few misspell * Minor word change in readme * Fix spelling * Restyle * Restyle2 * Remove from wordlist and make link * Update src/tools/device-graph/matter-device-graph.py Co-authored-by: C Freeman <[email protected]> * Suppressed linter warnings * Rename readme file * Uppercase readme file * Added orphan to readme * Added line limet for cluster names and fixed arrow parameter * Added handling for long cluster names and improved tree view * Added output details in readme --------- Co-authored-by: C Freeman <[email protected]>
- Loading branch information
1 parent
219c3f0
commit b1bf11e
Showing
4 changed files
with
291 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -634,6 +634,7 @@ GPL | |
GPLv | ||
Gradle | ||
gradlew | ||
graphviz | ||
Groupcast | ||
GroupId | ||
GroupKeyManagement | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
--- | ||
orphan: true | ||
--- | ||
|
||
# Setup | ||
|
||
This tool uses the python environment used by the python_testing efforts, which | ||
can be built using the below command. Notice that graphviz is required as an | ||
extra package in order for the tool to generate the graph file. | ||
|
||
``` | ||
scripts/build_python.sh -m platform -i out/python_env --extra_packages graphviz | ||
``` | ||
|
||
Once the python environment is build it can be activated using this command: | ||
|
||
``` | ||
source out/python_env/bin/activate | ||
``` | ||
|
||
# How to run | ||
|
||
When the python environment is activated the tool can be started as a regular | ||
python script. The tool does rely on the "framework" used for python testing, | ||
which means it is possible to do the commissioning of the DUT as well. | ||
|
||
By adding the appropriate parameters to the script execution, it will | ||
automatically perform a commissioning before running the tool itself. | ||
|
||
This is an example of running the test including commissioning a Thread based | ||
example app device | ||
|
||
``` | ||
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 | ||
``` | ||
|
||
In case the setup code and discriminator is not available, the QR code can also | ||
be used: | ||
|
||
``` | ||
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 | ||
``` | ||
|
||
In case the device uses a development PAA, the following parameter should be | ||
added. | ||
|
||
``` | ||
--paa-trust-store-path credentials/development/paa-root-certs | ||
``` | ||
|
||
In case the device uses a production PAA, the following parameter should be | ||
added. | ||
|
||
``` | ||
--paa-trust-store-path credentials/production/paa-root-certs | ||
``` | ||
|
||
Once a commissioning is completed for the device, is is possible to rerun the | ||
tool again for an already commissioned devices, this is an example of how to do | ||
so: | ||
|
||
``` | ||
python3 '/Users/renejosefsen/Developer/GitData/connectedhomeip/src/tools/device-graph/matter-device-graph.py' | ||
``` | ||
|
||
The tool currently outputs the dot file in this folder and the output file is | ||
named "matter-device-graph.dot". | ||
|
||
# How to view graph | ||
|
||
In order to view the graph, any tool that renders dot/graphviz files can be | ||
used. | ||
|
||
It is possible to open dot files and get them rendered in vscode using this | ||
extension: | ||
[vscode-graphviz](https://marketplace.visualstudio.com/items?itemName=joaompinto.vscode-graphviz) | ||
|
||
# Example of output | ||
|
||
This is an example of the graph outputted from a device: | ||
|
||
![matter device graph example](./matter-device-graph-example.png) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
# | ||
# Copyright (c) 2023 Project CHIP Authors | ||
# All rights reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
|
||
import os | ||
import pprint | ||
import sys | ||
|
||
import chip.clusters as Clusters | ||
import graphviz | ||
from rich.console import Console | ||
|
||
# Add the path to python_testing folder, in order to be able to import from matter_testing_support | ||
sys.path.append(os.path.abspath(sys.path[0] + "/../../python_testing")) | ||
from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main # noqa: E402 | ||
|
||
console = None | ||
maxClusterNameLength = 30 | ||
|
||
|
||
# Given there is currently no tranlation from DeviceTypeID to the device type name, | ||
# this dict is created for now. When some more general is available, it should be updated to use this. | ||
deviceTypeDict = { | ||
22: "Root Node", | ||
17: "Power Source", | ||
18: "OTA Requestor", | ||
20: "OTA Provider", | ||
14: "Aggregator", | ||
19: "Bridged Node", | ||
256: "On/Off Light", | ||
257: "Dimmable Light", | ||
268: "Color Temperature Light", | ||
269: "Extended Color Light", | ||
266: "On/Off Plug-in Unit", | ||
267: "Dimmable Plug-In Unit", | ||
771: "Pump", | ||
259: "On/Off Light Switch", | ||
260: "Dimmer Switch", | ||
261: "Color Dimmer Switch", | ||
2112: "Control Bridge", | ||
772: "Pump Controller", | ||
15: "Generic Switch", | ||
21: "Contact Sensor", | ||
262: "Light Sensor", | ||
263: "Occupancy Sensor", | ||
770: "Temperature Sensor", | ||
773: "Pressure Sensor", | ||
774: "Flow Sensor", | ||
775: "Humidity Sensor", | ||
2128: "On/Off Sensor", | ||
10: "Door Lock", | ||
11: "Door Lock Controller", | ||
514: "Window Covering", | ||
515: "Window Covering Controller", | ||
768: "Heating/Cooling Unit", | ||
769: "Thermostat", | ||
43: "Fan", | ||
35: "Casting Video Player", | ||
34: "Speaker", | ||
36: "Content App", | ||
40: "Basic Video Player", | ||
41: "Casting Video Client", | ||
42: "Video Remote Control", | ||
39: "Mode Select", | ||
45: "Air Purifier", | ||
44: "Air Quality Sensor", | ||
112: "Refrigerator", | ||
113: "Temperature Controlled Cabinet", | ||
114: "Room Air Conditioner", | ||
115: "Laundry Washer", | ||
116: "Robotic Vacuum Cleaner", | ||
117: "Dishwasher", | ||
118: "Smoke CO Alarm" | ||
} | ||
|
||
|
||
def AddServerOrClientNode(graphSection, endpoint, clusterName, color, nodeRef): | ||
|
||
if (len(clusterName) > maxClusterNameLength): | ||
clusterNameAdjustedLength = clusterName[:maxClusterNameLength] + '...' | ||
else: | ||
clusterNameAdjustedLength = clusterName | ||
|
||
graphSection.node(f"ep{endpoint}_{clusterName}", label=f"{clusterNameAdjustedLength}", style="filled,rounded", | ||
color=color, shape="box", fixedsize="true", width="3", height="0.5") | ||
graphSection.edge(nodeRef, f"ep{endpoint}_{clusterName}", style="invis") | ||
|
||
|
||
def CreateEndpointGraph(graph, graphSection, endpoint, wildcardResponse): | ||
|
||
numberOfRowsInEndpoint = 2 | ||
|
||
partsListFromWildcardRead = wildcardResponse[endpoint][Clusters.Objects.Descriptor][Clusters.Objects.Descriptor.Attributes.PartsList] | ||
|
||
listOfDeviceTypes = [] | ||
for deviceTypeStruct in wildcardResponse[endpoint][Clusters.Objects.Descriptor][Clusters.Objects.Descriptor.Attributes.DeviceTypeList]: | ||
try: | ||
listOfDeviceTypes.append(deviceTypeDict[deviceTypeStruct.deviceType]) | ||
except KeyError: | ||
listOfDeviceTypes.append(deviceTypeStruct.deviceType) | ||
|
||
# console.print(f"Endpoint: {endpoint}") | ||
# console.print(f"DeviceTypeList: {listOfDeviceTypes}") | ||
# console.print(f"PartsList: {partsListFromWildcardRead}") | ||
|
||
endpointLabel = f"Endpoint: {endpoint}\lDeviceTypeList: {listOfDeviceTypes}\lPartsList: {partsListFromWildcardRead}\l" # noqa: W605 | ||
|
||
nextNodeRef = "" | ||
nodeRef = f"ep{endpoint}" | ||
clusterColumnCount = 0 | ||
|
||
graphSection.node(f"ep{endpoint}", label=endpointLabel, style="filled,rounded", | ||
color="dodgerblue", shape="box", fixedsize="true", width="4", height="1") | ||
|
||
for clusterId in wildcardResponse[endpoint][Clusters.Objects.Descriptor][Clusters.Objects.Descriptor.Attributes.ServerList]: | ||
clusterColumnCount += 1 | ||
|
||
try: | ||
clusterName = Clusters.ClusterObjects.ALL_CLUSTERS[clusterId].__name__ | ||
except KeyError: | ||
clusterName = f"Custom server\l0x{clusterId:08X}" # noqa: W605 | ||
|
||
AddServerOrClientNode(graphSection, endpoint, clusterName, "olivedrab", nodeRef) | ||
|
||
if clusterColumnCount == 2: | ||
nextNodeRef = f"ep{endpoint}_{clusterName}" | ||
elif clusterColumnCount == 3: | ||
nodeRef = nextNodeRef | ||
clusterColumnCount = 0 | ||
numberOfRowsInEndpoint += 1 | ||
|
||
for clusterId in wildcardResponse[endpoint][Clusters.Objects.Descriptor][Clusters.Objects.Descriptor.Attributes.ClientList]: | ||
clusterColumnCount += 1 | ||
|
||
try: | ||
clusterName = Clusters.ClusterObjects.ALL_CLUSTERS[clusterId].__name__ | ||
except KeyError: | ||
clusterName = f"Custom client\l0x{clusterId:08X}" # noqa: W605 | ||
|
||
AddServerOrClientNode(graphSection, endpoint, clusterName, "orange", nodeRef) | ||
|
||
if clusterColumnCount == 2: | ||
nextNodeRef = f"ep{endpoint}_{clusterName}" | ||
elif clusterColumnCount == 3: | ||
nodeRef = nextNodeRef | ||
clusterColumnCount = 0 | ||
numberOfRowsInEndpoint += 1 | ||
|
||
if endpoint != 0: | ||
# Create link to endpoints in the parts list | ||
for part in partsListFromWildcardRead: | ||
graph.edge(f"ep{endpoint}", f"ep{part}", ltail=f"cluster_{endpoint}", minlen=f"{numberOfRowsInEndpoint}") | ||
|
||
|
||
class TC_MatterDeviceGraph(MatterBaseTest): | ||
@async_test_body | ||
async def test_matter_device_graph(self): | ||
|
||
# Create console to print | ||
global console | ||
console = Console() | ||
|
||
# Run descriptor validation test | ||
dev_ctrl = self.default_controller | ||
|
||
# Perform wildcard read to get all attributes from device | ||
console.print("[blue]Capturing data from device") | ||
wildcardResponse = await dev_ctrl.ReadAttribute(self.dut_node_id, [('*')]) | ||
# console.print(wildcardResponse) | ||
|
||
# Creating graph object | ||
deviceGraph = graphviz.Digraph() | ||
deviceGraph.attr(style="rounded", splines="line", compound="true") | ||
|
||
console.print("[blue]Generating graph") | ||
# Loop through each endpoint in the response from the wildcard read | ||
for endpoint in wildcardResponse: | ||
|
||
if endpoint == 0: | ||
with deviceGraph.subgraph(name='cluster_rootnode') as rootNodeSection: | ||
CreateEndpointGraph(deviceGraph, rootNodeSection, endpoint, wildcardResponse) | ||
else: | ||
with deviceGraph.subgraph(name='cluster_endpoints') as endpointsSection: | ||
with endpointsSection.subgraph(name=f'cluster_{endpoint}') as endpointSection: | ||
CreateEndpointGraph(deviceGraph, endpointSection, endpoint, wildcardResponse) | ||
|
||
deviceGraph.save(f'{sys.path[0]}/matter-device-graph.dot') | ||
|
||
deviceDataFile = open(f'{sys.path[0]}/matter-device-data.txt', 'w') | ||
deviceDataFile.write(pprint.pformat((wildcardResponse))) | ||
deviceDataFile.close() | ||
|
||
|
||
if __name__ == "__main__": | ||
default_matter_test_main() |