ZigBee network map when using only the ZigBee Binding

Hi @chris and @curlyel,
Indeed the information I use as input is produced by the ZigBee binding and it would be great if such a network map could be incorporated in the binding so that it could be displayed in the BasicUI or any other place.
I am not familiar with binding development so I have to ask, would it be possible to re-use what I have done in the binding or it has to be written from scratch? If the former is the case then it would be great and I would be happy to follow @curlyel sugestion.

BR,
Nikos

Just for info, this is what I did for the customer a few years back -:

Clicking on a node shows the node properties. The graph is updated as information is updated, and nodes can be dragged around (they have a kind of gravity function to align them).

PaperUI theming is the Eclipse SmartHome theme.

If I can find the source to this I will see if I can make it available - someone might want to take it and add it to PaperUI (or the next-gen PaperUI).


4 Likes

This script worked very well for me, but I needed additional info, so here’s my solution.
Major differences: work directly with the OpenHAB API to get the most up-to-date info, no more sed, detailed route/neighbor info, extensive color coding, use circo instead of dot for visualization, simpler to extend.

#/bin/bash

dir=$(mktemp -d)

wget -q http://localhost:8080/rest/things -O $dir/things.json

# Replace with the "strict" version if it's too dense.
echo "digraph G {" >> $dir/network.dot
#echo "strict digraph G {" >> $dir/network.dot

echo "edge [decorate=true]" >> $dir/network.dot
echo "node [shape=box]" >> $dir/network.dot
echo "0 [label=<0<br/>COORDINATOR> color=blue]" >> $dir/network.dot

jq -r '.[] |
  select(.thingTypeUID == "zigbee:device") |
  .properties.zigbee_networkaddress + " " +
  "[" +
    "label=<" +
    .properties.modelId + "<br/>" +
    .properties.zigbee_networkaddress + "<br/>" +
    (.properties.zigbee_macaddress // .configuration.zigbee_macaddress) + "<br/>" +
    .properties.zigbee_logicaltype + "<br/>" +
    .label + "<br/>" +
    .location +
    ">" +
    "color=" +
    if .statusInfo.status == "ONLINE" then "green"
    elif .statusInfo.status == "OFFLINE" then "orange"
    elif .statusInfo.status == "UNKNOWN" then "red"
    else "black" end +
  "]"
' $dir/things.json >> $dir/network.dot

nodes=$(jq -r '.[] |
  select(.properties.zigbee_networkaddress != null) |
  .properties.zigbee_networkaddress
' $dir/things.json)

for node in ${nodes}; do
  jq -r --arg node $node '.[] |
    select(.properties.zigbee_networkaddress == $node) |
    select(.properties.zigbee_routes != "[]" and .properties.zigbee_routes != null) |
    .properties.zigbee_routes
  ' $dir/things.json |
  jq -r --arg node $node '.[] |
    $node + " -> " +
    if .destination != .next_hop then .next_hop + " -> " else "" end +
    .destination +
    " [" +
    "color=" +
    if .state == "ACTIVE" then "green"
    elif .state == "DISCOVERY_UNDERWAY" then "orange"
    elif .state == "DISCOVERY_FAILED" then "red"
    elif .state == "INACTIVE" then "blue"
    else "yellow" end + " " +
    "]"
  ' >> $dir/network.dot

  jq -r --arg node $node '.[] |
    select(.properties.zigbee_networkaddress == $node) |
    select(.properties.zigbee_neighbors != "[]" and .properties.zigbee_neighbors != null) |
    .properties.zigbee_neighbors
  ' $dir/things.json |
  jq -r --arg node $node '.[] |
    $node + " -> " + .address + " " +
    "[" +
      "fontcolor=" +
      if .joining == "ENABLED" then "green"
      elif .joining == "DISABLED" then "black"
      elif .joining == "UNKNOWN" then "red"
      else "blue" end + " " +
      "label=<" +
      .lqi + "/" + .depth +
      ">" +
    "]"
  ' >> $dir/network.dot
done

echo "label=\"`date +%Y-%m-%d\ %H:%M`\";" >> $dir/network.dot

echo "}" >> $dir/network.dot

/usr/bin/circo -Tpng $dir/network.dot -o /etc/openhab2/html/zigbee_network.png

rm -rf $dir

Example graph

Example graph (strict mode)

2 Likes

I only get a box wtih COORDINATOR…
probably something is changed.
No routes and no neighbors are found

probably something is changed.
No routes and no neighbors are found

Hi Stefan,
The script is working for me using openhab 2.5.4 on openhabian. Let us try to troubleshoot why it is not working for you. I suggest the following steps:

  1. Check in Paper UI that in your Zigbee Things properties you can see neighbors and routes
  2. Run the jq commands without any post-processing with sed and grep and see if you get any output
    e.g.
  • zigbee nodes information
    jq '.[] | select(.value.properties.modelId != null) | .value.properties.zigbee_networkaddress + ",[shape=record,label=^{" + .value.properties.modelId + "|" + .value.properties.zigbee_networkaddress +"|"+ .value.properties.zigbee_logicaltype +"}^]"' Thing.json
  • zigbee nodes routing
    jq '.[] | select(.value.properties.zigbee_networkaddress != null) | select (.value.properties.zigbee_routes != "[]") | .value.properties.zigbee_networkaddress, .value.properties.zigbee_routes' Thing.json
  • zigbee nodes neighbors
    jq '.[] | select(.value.properties.zigbee_networkaddress != null) | select (.value.properties.zigbee_neighbors != "[]") | .value.properties.zigbee_networkaddress, .value.properties.zigbee_neighbors' Thing.json

* Where Thing.json is your backup copy of the latest Things backup file from /var/lib/openhab2/jsondb/backup

Maybe I found the problem.
I use the HUE binding(for my huebridge) and the DECONZ binding(for my deconz usb stick) for my zigbee devices.
There’s also a zigbee binding. Maybe you are using the zigbee binding?

Indeed, this method is for the Zigbee binding as noted in the topic’s title and the 1st paragraph of the initial post. :slight_smile:

In this post I would like to share a method I am using to create a ZigBee network map while using the ZigBee Binding.

my fault :frowning:

The current implementation shows only the details of devices of type zigbee:device. I’ve added a small fix to also show devices with static thing definition.
I’ve also added an option to draw the graph with dot since it might be cleaner looking for some people

#/bin/bash

dir=$(mktemp -d)
#dir=./zigbee_map

wget -q http://localhost:8080/rest/things -O $dir/things.json

# Replace with the "strict" version if it's too dense.
#echo "digraph G {" >> $dir/network.dot
echo "strict digraph G {" >> $dir/network.dot

echo "edge [decorate=true]" >> $dir/network.dot
echo "node [shape=box]" >> $dir/network.dot
echo "0 [label=<0<br/>COORDINATOR> color=blue]" >> $dir/network.dot

jq -r '.[] |
  select(.thingTypeUID | startswith("zigbee:")) |
  .properties.zigbee_networkaddress + " " +
  "[" +
    "label=<" +
    .properties.modelId + "<br/>" +
    .properties.zigbee_networkaddress + "<br/>" +
    (.properties.zigbee_macaddress // .configuration.zigbee_macaddress) + "<br/>" +
    .properties.zigbee_logicaltype + "<br/>" +
    .label + "<br/>" +
    .location +
    ">" +
    "color=" +
    if .statusInfo.status == "ONLINE" then "green"
    elif .statusInfo.status == "OFFLINE" then "orange"
    elif .statusInfo.status == "UNKNOWN" then "red"
    else "black" end +
  "]"
' $dir/things.json >> $dir/network.dot

nodes=$(jq -r '.[] |
  select(.properties.zigbee_networkaddress != null) |
  .properties.zigbee_networkaddress
' $dir/things.json)

for node in ${nodes}; do
  jq -r --arg node $node '.[] |
    select(.properties.zigbee_networkaddress == $node) |
    select(.properties.zigbee_routes != "[]" and .properties.zigbee_routes != null) |
    .properties.zigbee_routes
  ' $dir/things.json |
  jq -r --arg node $node '.[] |
    $node + " -> " +
    if .destination != .next_hop then .next_hop + " -> " else "" end +
    .destination +
    " [" +
    "color=" +
    if .state == "ACTIVE" then "green"
    elif .state == "DISCOVERY_UNDERWAY" then "orange"
    elif .state == "DISCOVERY_FAILED" then "red"
    elif .state == "INACTIVE" then "blue"
    else "yellow" end + " " +
    "]"
  ' >> $dir/network.dot

  jq -r --arg node $node '.[] |
    select(.properties.zigbee_networkaddress == $node) |
    select(.properties.zigbee_neighbors != "[]" and .properties.zigbee_neighbors != null) |
    .properties.zigbee_neighbors
  ' $dir/things.json |
  jq -r --arg node $node '.[] |
    $node + " -> " + .address + " " +
    "[" +
      "fontcolor=" +
      if .joining == "ENABLED" then "green"
      elif .joining == "DISABLED" then "black"
      elif .joining == "UNKNOWN" then "red"
      else "blue" end + " " +
      "label=<" +
      .lqi + "/" + .depth +
      ">" +
    "]"
  ' >> $dir/network.dot
done

echo "label=\"`date +%Y-%m-%d\ %H:%M`\";" >> $dir/network.dot

echo "}" >> $dir/network.dot

# draw graph using dot or circo
/usr/bin/circo -Tpng $dir/network.dot -o ./zigbee_network.png
#dot -Tpng $dir/network.dot -o ./zigbee_network.png

rm -rf $dir

1 Like

If someone is interested, here is a pure python version of the script without any file creation or copy but you need to install graphviz python interface

#!/usr/bin/python3
import json
import sys
from graphviz import Digraph

def main(argv):
    if(len(sys.argv) < 2):
        print('missing input file')
        sys.exit(2)
    inputfile = str(sys.argv[1])
    outputfile = 'zigbee_map'
    if(len(sys.argv) >= 3):
        outputfile = str(sys.argv[2])

    file = open(inputfile)
    json_string = file.read()
    file.close()
    dot = Digraph('graph',comment='Zigbee map',graph_attr={'rankdir':'LR'},node_attr={'shape': 'record'},format='png',filename=outputfile)
    json_dictionary = json.loads(json_string)
    for key in json_dictionary:
        if(key.find("zigbee:")>=0):
            zigbee_logicaltype=json_dictionary[key]["value"]["properties"]["zigbee_logicaltype"]
            zigbee_networkaddress =json_dictionary[key]["value"]["properties"]["zigbee_networkaddress"]
            label = json_dictionary[key]["value"]["label"]
            if(zigbee_logicaltype=="COORDINATOR"):
                zigbee_macaddress=json_dictionary[key]["value"]["properties"]["zigbee_macaddress"]
                modelId =""
            else:
                zigbee_macaddress=json_dictionary[key]["value"]["configuration"]["properties"]["zigbee_macaddress"]
                modelId =json_dictionary[key]["value"]["properties"]["modelId"]
            dot_label=str()
            dot_label+=zigbee_logicaltype
            dot_label+="|"+label
            dot_label+="|"+modelId
            dot_label+="|{adress |"+zigbee_networkaddress+"}"
            dot_label+="|{mac |"+zigbee_macaddress+"}"
            dot.node(zigbee_networkaddress, dot_label)

            zigbee_networkaddress =json_dictionary[key]["value"]["properties"]["zigbee_networkaddress"]
            zigbee_neighbors_json =json_dictionary[key]["value"]["properties"]["zigbee_neighbors"]
            zigbee_neighbors_list = json.loads(zigbee_neighbors_json)
            for item in zigbee_neighbors_list:
                dot.edge(zigbee_networkaddress, item['address'], label=item['joining'], color='blue')
            zigbee_routes_json =json_dictionary[key]["value"]["properties"]["zigbee_routes"]
            zigbee_routes_list = json.loads(zigbee_routes_json)
            for item in zigbee_routes_list:
                dot.edge(item['next_hop'], item['destination'], label=item['state'], color='black')
    dot.render()

if __name__ == "__main__":
   main(sys.argv[1:])

Do you have any reason to use a backup or a request and not directly the jsondb file /var/lib/openhab2/jsondb/org.eclipse.smarthome.core.thing.Thing.json ?

I was able to produce a map in /tmp/zigbee.png with the command
./zigbee_map.py /var/lib/openhab2/jsondb/org.eclipse.smarthome.core.thing.Thing.json /tmp/zigbee

Hi all,

I’m about to move from zigbee2mqtt to the openHAB zigbee binding as my new coordinator isn’t supported by z2m. Great to see the activity in this thread :wink:

I’m using openHAB 3.0.1 and I wonder (before I try to run the script) if v3 is supported? In the main UI I see the values below in blue, for both my xiaomi zigbee power plug (ROUTER) and contact sensor (END_DEVICE)

Hi Stefan,
I have not migrated to OpenHAB 3 yet so I have not tried any of the methods mentioned here on the new version. Nevertheless, you could try to check whether the files or the REST API endpoints are available.

thanks @ndasyras for the reply. I tested it with latest modification from @abol3z

Below you can find my modified version to make it work with openHAB 3.x (which by default requires the use of an authentication token with REST API). As such I’ve replaced wget with curl (and added 3 variables to set the openHAB host, port and APIToken).

One question: Any idea when and how often the binding updates the routes and neighbor info?

#/bin/bash

openHABHost="192.168.XXX.XXX"
openHABPort="8080"
APIToken="qbmkfBVCTc2VL6g4-1kM1chuD5vZNDd38w....."

dir=$(mktemp -d)
#dir=.

curl -s -X GET "http://$openHABHost:$openHABPort/rest/things" -H "accept: application/json" -H "Authorization: Bearer $APIToken" -o $dir/things.json

# Replace with the "strict" version if it's too dense.
#echo "digraph G {" >> $dir/network.dot
echo "strict digraph G {" >> $dir/network.dot

echo "edge [decorate=true]" >> $dir/network.dot
echo "node [shape=box]" >> $dir/network.dot
echo "0 [label=<0<br/>COORDINATOR> color=blue]" >> $dir/network.dot

jq -r '.[] |
  select(.thingTypeUID | startswith("zigbee:")) |
  .properties.zigbee_networkaddress + " " +
  "[" +
    "label=<" +
    .properties.modelId + "<br/>" +
    .properties.zigbee_networkaddress + "<br/>" +
    (.properties.zigbee_macaddress // .configuration.zigbee_macaddress) + "<br/>" +
    .properties.zigbee_logicaltype + "<br/>" +
    .label + "<br/>" +
    .location +
    ">" +
    "color=" +
    if .statusInfo.status == "ONLINE" then "green"
    elif .statusInfo.status == "OFFLINE" then "orange"
    elif .statusInfo.status == "UNKNOWN" then "red"
    else "black" end +
  "]"
' $dir/things.json >> $dir/network.dot

nodes=$(jq -r '.[] |
  select(.properties.zigbee_networkaddress != null) |
  .properties.zigbee_networkaddress
' $dir/things.json)

for node in ${nodes}; do
  jq -r --arg node $node '.[] |
    select(.properties.zigbee_networkaddress == $node) |
    select(.properties.zigbee_routes != "[]" and .properties.zigbee_routes != null) |
    .properties.zigbee_routes
  ' $dir/things.json |
  jq -r --arg node $node '.[] |
    $node + " -> " +
    if .destination != .next_hop then .next_hop + " -> " else "" end +
    .destination +
    " [" +
    "color=" +
    if .state == "ACTIVE" then "green"
    elif .state == "DISCOVERY_UNDERWAY" then "orange"
    elif .state == "DISCOVERY_FAILED" then "red"
    elif .state == "INACTIVE" then "blue"
    else "yellow" end + " " +
    "]"
  ' >> $dir/network.dot

  jq -r --arg node $node '.[] |
    select(.properties.zigbee_networkaddress == $node) |
    select(.properties.zigbee_neighbors != "[]" and .properties.zigbee_neighbors != null) |
    .properties.zigbee_neighbors
  ' $dir/things.json |
  jq -r --arg node $node '.[] |
    $node + " -> " + .address + " " +
    "[" +
      "fontcolor=" +
      if .joining == "ENABLED" then "green"
      elif .joining == "DISABLED" then "black"
      elif .joining == "UNKNOWN" then "red"
      else "blue" end + " " +
      "label=<" +
      .lqi + "/" + .depth +
      ">" +
    "]"
  ' >> $dir/network.dot
done

echo "label=\"`date +%Y-%m-%d\ %H:%M`\";" >> $dir/network.dot

echo "}" >> $dir/network.dot

# draw graph using dot or circo
/usr/bin/circo -Tpng $dir/network.dot -o ./zigbee_network.png
dot -Tpng $dir/network.dot -o ./zigbee_network.png

rm -rf $dir
1 Like

In 2.5.8 zigbee Thing information seems to be updated once per day. Check in openhab events log for a line like this: “Thing ‘zigbee:coordinator_xxx’ has been updated.”

1 Like

I think in OH3 it’s even possible to configure this update. I will try now with 5min (just for testing)

Same for me. Only with a update of 5 minutes, I get the data between the zigbee things. Otherwise not. Is it a bad thing for battery live to set this to 5 minutes permanently?

Hi Folks,
based on the scripts and ideas of this thread, I’ve created a completely new script that is not affected by the problems described here (like update cycle, etc.). My approach also gets rid of the problem with some coordinators that are not showing the neighbor data.

My new script does not use the REST Api, but instead uses the built-in zigbee commands in the openhab console. The script is written in python and only needs standard plugins that should be available everywhere.

Cons: As the script accesses the console several times for 1 complete run, it takes some seconds to complete, depending on the number of nodes in the network. In my case it takes about 20s.

This the output, that comes out on my zigbee network (Engine “neato”):
zigbee_map_neato_240114-1116

Engine “dot”:
zigbee_map_dot_240114-1401

Here is the script source:

#!/usr/bin/env python3

#import json # Only for debugging purposes
import re
import subprocess
import graphviz
from datetime import datetime
import os

output_filename = 'zigbee_map'
output_dir = '/etc/openhab/html/'
output_format = 'svg' #png, svg, dot
output_node_shape = 'box' # box, ellipse, circle, diamond, house, hexagon, octagon
output_line_shape = 'curved' # none, line, polyline, curved, ortho, spline
output_engine = 'neato' # dot, fdp, neato, circo
pwd = "Enter your console password here"   # -------------------- Change before usage! ----------------

zigbee_data = [];
cmd_basic = ["openhab-cli","console", "-p", pwd]
cmd_nodes = cmd_basic + ["zigbee", "nodes"]
cmd_neighbor = cmd_basic + ["zigbee", "neighbors"]
cmd_list_things = cmd_basic + ["things", "list"]
cmd_show_thing = cmd_basic + ["things", "show"]

# Zigbee properties
NETWORK_ADDR_SOURCE='network'
NETWORK_ADDR='network address'
MODEL = 'model'
IEEE_ADDR = 'ieee address'
LOGICAL_TYPE='logical type'
STATE = 'state'
LQI = 'lqi'
DEVICE_TYPE = 'device type'
RELATIONSHIP = 'relationship'
RELATIONSHIP_PARENT = 'parent'
RELATIONSHIP_SIBLING = 'sibling'

# Thing properties
LABEL = 'label'
STATUS = 'status'
ZB_NETWORK_ADDR = 'zigbee_networkaddress'

def export(filename, content):
    file = open(filename, 'w')
    file.write(content)
    file.close()
    return

def parse_nodes(raw_data):
    result_data =[];
    columns = []
    i = 0
    for line in raw_data.splitlines():
        if not line[0:8] == '        ':
            line = line.strip()                     # remove leading/trailing white spaces
            line = re.sub(r'\s\s+',r'\t', line)     # Replace multiple whitespaces by a tab as separator
            if line:
                if i == 0:
                    columns = re.sub(NETWORK_ADDR_SOURCE, NETWORK_ADDR, line.strip().lower()).split('\t')
                    if len(columns)>3:
                        i = i + 1
                else:
                    d = {}                          # dictionary to store file data (each line)
                    data = [item.strip() for item in line.split('\t')]
                    for index, elem in enumerate(data):
                        d[columns[index]] = data[index]
                    result_data.append(d)           # append dictionary to list
    return result_data

def parse_neighbor(raw_data):
    result_data = []
    columns = [];
    i = 0
    for line in raw_data.splitlines():
        line = line.strip()                     # remove leading/trailing white spaces
        line = re.sub(r'\s\s+',r'\t', line)     # Replace multiple whitespaces by a tab as separator
        if i == 0:
            columns = line.lower().split('\t')
            if len(columns)>3:
                i = i + 1
        else:
            d = {}                              # dictionary to store file data (each line)
            data = [item.strip() for item in line.split('\t')]
            for index, elem in enumerate(data):
                d[columns[index]] = data[index]
            result_data.append(d)               # append dictionary to list
    return result_data

def get_nodes_raw_data():
    try:
        proc = subprocess.run(cmd_nodes, capture_output=True)
        nodes_raw_data = subprocess.check_output(cmd_nodes,stderr=subprocess.STDOUT).decode('utf-8')
        print("Nodes:\n" + nodes_raw_data)
    except subprocess.CalledProcessError as e:
        raise RuntimeError("command '{}' return with error (code {}): {}\nerror: {}".format(e.cmd, e.returncode, e.output, e.stderr))
    return nodes_raw_data

def get_neighbor_raw_data(network_ID):
    try:
        neighbors_raw_data = subprocess.check_output(cmd_neighbor+[network_ID], stderr=subprocess.STDOUT).decode('utf-8')
        #export('tmp/neighbor' + network_ID + '.txt',neighbors_raw_data)
    except subprocess.CalledProcessError as e:
        raise RuntimeError("command '{}' return with error (code {}): {}\nerror: {}".format(e.cmd, e.returncode, e.output, e.stderr))
    return neighbors_raw_data

def get_thing_raw_data(uid):
    try:
        thing_raw_data = subprocess.check_output(cmd_show_thing + [uid], stderr=subprocess.STDOUT).decode('utf-8')
        #export('tmp/things.txt',thing_raw_data)
    except subprocess.CalledProcessError as e:
        raise RuntimeError("command '{}' return with error (code {}): {}\nerror: {}".format(e.cmd, e.returncode, e.output, e.stderr))
    return thing_raw_data

def get_thingUIDs_raw_data():
    try:
        thingUIDs_raw_data = subprocess.check_output(cmd_list_things, stderr=subprocess.STDOUT).decode('utf-8')
        #export('tmp/things.txt',thingUIDs_raw_data)
    except subprocess.CalledProcessError as e:
        raise RuntimeError("command '{}' return with error (code {}): {}\nerror: {}".format(e.cmd, e.returncode, e.output, e.stderr))
    return thingUIDs_raw_data

def get_thingUIDs(raw_data):
    thingUIDs = []
    for line in raw_data.splitlines():
        if line.startswith('zigbee:coordinator') or line.startswith('zigbee:device'):
            thingUIDs.append(line.partition(' ')[0])
    return thingUIDs

def get_thing_information(thingUIDs):
    things = {}
    for thingUID in thingUIDs:
        print('Trying to get thing data for device: ' + thingUID)
        raw_data = get_thing_raw_data(thingUID)
        thing = {} # dictionary to store properties
        for line in raw_data.splitlines():
            elements = line.partition(':')
            key = elements[0].strip().lower()
            value = elements[2].strip()
            if (not key == '') and not key in thing:
                thing[key] = value
        if not ZB_NETWORK_ADDR in thing:
            thing[ZB_NETWORK_ADDR] = '0'    # Must be the coordinator
        things[thing[ZB_NETWORK_ADDR]] = thing
    return things

#============================================================================================
#                   Main
#============================================================================================
print('Retrieving and parsing all Zigbee nodes..')
zigbee_data = parse_nodes(get_nodes_raw_data())
print('Retrieving all Things..')
thingUIDs = get_thingUIDs(get_thingUIDs_raw_data())
things = get_thing_information(thingUIDs)
# for debugging purposes only:
#print(json.dumps(things, indent=4))

#for debugging purposes only:
#print(json.dumps(zigbee_data, indent=4))

dot = graphviz.Digraph(comment='Zigbee-Map', name='ZigBee-Map', engine=output_engine)
dot.format = output_format

for device in zigbee_data:
    network_ID = device[NETWORK_ADDR]
    caption=''
    if (MODEL in device):
        caption = device[MODEL] + "\n" + network_ID + "\n" + device[IEEE_ADDR] + "\n" + device[DEVICE_TYPE] + "\n" + things[network_ID][LABEL]
    else:
        caption = "\n" + network_ID + "\n" + device[IEEE_ADDR] + "\n" + things[network_ID][LABEL]
    boxcolor = 'darkgreen' if things[network_ID][STATUS] == 'ONLINE' else 'orange' if device[STATE]=='ONLINE' else 'red'
    dot.node(network_ID, caption, color=boxcolor)
    if (LOGICAL_TYPE in device) and ((device[LOGICAL_TYPE].lower()=="router") or (device[LOGICAL_TYPE].lower()=="coordinator")):
        print("Trying to find neighbors for Zigbee device with network Address: " + network_ID)
        neighbors = parse_neighbor(get_neighbor_raw_data(network_ID))
        print ('Found: {} neighbors'.format(len(neighbors)))
        device['Neighbors'] = neighbors

        for neighbor in neighbors:
            if (NETWORK_ADDR in neighbor):
                if neighbor[RELATIONSHIP].lower() == RELATIONSHIP_PARENT:
                    thickness = '2'
                elif neighbor[RELATIONSHIP].lower() == RELATIONSHIP_SIBLING:
                    thickness = '1'
                else:
                    thickness = '0.5'
                dot.node(neighbor[NETWORK_ADDR], shape=output_node_shape)
                dot.edge(network_ID, neighbor[NETWORK_ADDR], neighbor[LQI], color='red' if int(neighbor[LQI]) < 20 else 'black', penwidth=thickness)

dot.attr(label=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), root="0", splines=output_line_shape, overlap="scale")
dot.render(output_dir + output_filename)

I schedule the script via the executer addon in openhab.
Feedback would be nice, maybe we can improve it even more.
Have fun!

3 Likes

Hi! Have u evolved the project at all for OH4; im on a RPI4 and am looking to understand my network better.

Yes, the script is running well on openHAB 4.1.1