Skip to content

Commit

Permalink
Initial version of matter device graph tool (project-chip#30187)
Browse files Browse the repository at this point in the history
* 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
ReneJosefsen and cecille authored Jan 31, 2024
1 parent 219c3f0 commit b1bf11e
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ GPL
GPLv
Gradle
gradlew
graphviz
Groupcast
GroupId
GroupKeyManagement
Expand Down
82 changes: 82 additions & 0 deletions src/tools/device-graph/README.md
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.
208 changes: 208 additions & 0 deletions src/tools/device-graph/matter-device-graph.py
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()

0 comments on commit b1bf11e

Please sign in to comment.