
This page describes the Mesh graph page of the plugin Web interface, available from plugin version 9.1.008. It sits beside the two older charts documented in Network section, which are unchanged: the Topology wheel and the Topology graph still read the
/topologiereport, while the Mesh graph reads a second, richer report,/topology-scan.
The page is reached from the WebUI menu: Network > Mesh graph (/network/mesh).

The screenshots on this page are rendered from one real scan of a 62-device ZNP network - 25 of the 30 nodes that could answer did - so what they show is a real mesh rather than a mock-up. Each node carries the name you gave the device and its network id underneath; a device you never named is labelled with its network id alone. These pictures may have changed since this documentation was written.
The page is three blocks, top to bottom:

The Mesh graph card with the router Led #2 Bureau clicked. The LQI window has been narrowed to 50 - 215 and Hide nodes with no visible link switched on, so this is not how the page opens.
The card reads from top to bottom. Each area is described in detail further down this page; this section says what is where.
| Area | What it does | More |
|---|---|---|
| Focus a node | Type a name or a network id: the graph centres on that device and fades the rest. The quickest way to find one device on a large mesh | Layouts |
| Links to draw | Which table is drawn: Neighbour tables, Routing tables, Both or Associated devices. A table the scan does not carry is greyed out | Three tables |
| Layout | How the nodes are placed - cose, concentric, breadthfirst, circle, grid. Positions only; the data is the same | Layouts |
| LQI window | Keeps only the neighbour links whose LQI falls inside the two handles. The badge shows the current window | A note on LQI |
| Routes for | Pick a device to see a computed route: the coordinator for how it reaches every device, any other device for its route out to the coordinator. Empty when the page opens, so nothing is drawn until you pick | Computed routes |
| Work the routes out again | Works the route for the device in Routes for out again, against the scan on screen. Greyed out while the field is empty. The text beside it says what the route on screen is | Computed routes |
| Back to the coordinator | Shown only while one device’s route is on screen: puts the coordinator back in Routes for, so the graph shows how it reaches every device | Computed routes |
| Relationship checkboxes | Parent / child, Sibling, Other relationships, Unknown relationship: which neighbour links are drawn, by what the reporting node says the neighbour is to it | The relationships |
| Hide end devices | Removes the leaves, to leave the backbone of routers | Filters |
| Hide nodes with no visible link | Removes whatever the other filters have left isolated | Filters |
| Device icons | Draws each node with the icon of what the device is, or as a plain shape | Reading the picture |
The five buttons at the right, above the graph, from left to right:
| Button | What it does |
|---|---|
| Zoom in / Zoom out | The same as the mouse wheel |
| Fit to view | Brings the whole graph back into the frame after zooming or panning |
| Recompute layout | Runs the current layout again - useful after dragging nodes around, or when a layout settled badly |
| Fill the screen | Puts the graph fullscreen. The same buttons stay available there, the last one becoming Leave fullscreen |
Zoom, pan and drag it. Each node carries its name and its network id; a node with no name carries its network id alone. Click something to open its panel underneath:
The node clicked is drawn with a thick dark border, and its links stand out from the rest of the mesh. In the screenshot Led #2 Bureau is the selected node; the red square is a different thing, a device that is not reachable.
Under the graph, a line reminds how nodes are labelled, and an amber line counts the report rows that could not be drawn because they name a node the scan does not list - see Reading the picture.
Clicking a node opens a panel under the graph, titled with its name and its network id. Two buttons sit in its title bar:
The panel has three parts.
The identity block. Network id and IEEE address, the type the node plays in the mesh and its model, the ZDeviceID with the icon, family and endpoint it was resolved from, the Scan status (the ⓘ beside it explains the value - Answered, Timed out, No neighbour table, Not scanned), Rx on when idle, Health, Battery, LQI and RSSI. Then the association, from the Associated devices table:
The note under the block matters: health, model, LQI, RSSI and battery are this device’s values now, not at the time of the scan, and the LQI there is the one the coordinator recorded for the device - a link’s own LQI is on the link.
Neighbour table rows. Every neighbour row that mentions this node, in either direction:
| Column | What it holds |
|---|---|
| Reading | who measured what: “Volet Fifi hears Led #2 Bureau”. The same pair can appear twice, once per end - Led #2 Bureau hears Led #1 Bureau and Led #1 Bureau hears Led #2 Bureau |
| Neighbour | the node at the other end |
| Relationship | what the reporter says the neighbour is to it |
| LQI | the reporter’s reading, with its colour band |
| Reported type, Depth, Rx on idle, Permit joining | the rest of the row, as the reporter gave it |
Neighbour, Relationship and LQI can be sorted by clicking their header. Ten rows are shown at a time, and the selector beside the pager offers 10, 25 or 50. Read the Depth column as a whole rather than row by row: a column of 15 is a radio with no depth to give - see Depth.
Routing table rows. Every routing row in which this node plays a part, and the Role column says which part:
| Role | Meaning |
|---|---|
| Reports the route | this node’s own routing table: to reach that destination, I hand the frame to that next hop |
| Is the next hop | another node hands frames to this one - in the screenshot, the coordinator reaches Prise RPI, Prise Info (Mac) and others through Led #2 Bureau |
| Is the destination | another node holds a route to this one |
Then Reporter, Destination, Next hop, the route Status, and the Flags - many-to-one, memory constrained, route record required. See Routing versus Neighbours for what they mean, and why a routing row carries no LQI.
Zigbee is a mesh network, not a star. A frame does not have to reach the coordinator directly: it can be relayed, hop by hop, by any mains-powered device on the way. That is what the word means here - the network is a web of short radio links rather than a set of spokes, and it re-forms itself around a link that degrades.
Three roles make that possible (Zigbee specification, and Silicon Labs - Network Node Types):
| Role | Also called | Relays frames | Keeps a neighbour table | Radio |
|---|---|---|---|---|
| Coordinator | ZC | yes | yes | always on |
| Router | ZR, FFD - Full-Function Device | yes | yes | always on |
| End device | ZED, RFD - Reduced-Function Device | no | no | often asleep |
The word “usually” above is doing real work, and it is the first thing to check when a device is not where you expect it in the graph.
A device is a router because it says it is - in the MAC capability it announces when it joins - not because of how it is powered. Several common families are mains-powered and still announce themselves as reduced-function, so they relay nothing and keep no neighbour table:
The plugin believes the device’s own announcement, which is the right call: it gets these models right without a list of exceptions. For the graph this means such a device is drawn as an end device, is reported as “No neighbour table” on a scan, and - like any end device - appears in the associated devices view under the router it is attached to rather than as part of the backbone.
So a room whose only “mains-powered” device is a no-neutral switch has no relay in it, however many Zigbee devices are on the wall. That is worth knowing before blaming the radio.
Two consequences run through the whole of this page:
The Mesh graph draws that backbone, the links between the nodes that form it, and - when the scan collected it - which router each battery device is attached to.
A Zigbee node does not know “the network”. It knows a few tables about its own surroundings, and a topology report is those tables, collected node by node and stitched together. The Mesh graph can draw three of them, and they answer three genuinely different questions:
| Table | Zigbee request | Question it answers | Carries |
|---|---|---|---|
| Neighbours | Mgmt_Lqi_req (ZDP cluster 0x0031), answered by Mgmt_Lqi_rsp (0x8031, spec §2.4.4.3.2) |
Who can this node hear, and how well? | LQI, relationship, device type, depth, Rx-on-when-idle, permit-joining |
| Routing | Mgmt_Rtg_req (0x0032), answered by Mgmt_Rtg_rsp (0x8032, §2.4.4.3.3) |
To reach X, who does this node hand the frame to? | destination, next hop, route status, many-to-one / memory-constrained / route-record flags |
| Associated devices | NWK_addr_req with RequestType 0x01 - the extended address request - answered by NWK_addr_rsp with NumAssocDev / NWKAddrAssocDevList |
Which devices are this router’s children? | a plain list of network addresses, nothing else |
They overlap, but none of them is a substitute for another: two nodes can be neighbours with no route between them, a route can exist through a node that is not in the neighbour table, and the the associated device list is in neither.
The Links to draw control on the page selects which of them is drawn. They are exclusive modes rather than overlays, because a pair of nodes can appear in all three at once and the picture would say nothing.
Every control this page mentions - what it acts on, and what it is set to when the page opens - is listed together under Filters and what they act on. The plugin settings that decide what a scan collects in the first place are under Settings.
A mode is only offered when the selected scan actually carries that table. The scan listing says what each one holds, and a control for a table that is absent is greyed out rather than drawing an empty graph.
Every row of a neighbour table carries a relationship field: what the reporting node believes the neighbour is to it. The Zigbee specification defines five values, and the plugin adds a sixth for a value it cannot decode:
| Value | Meaning |
|---|---|
parent |
the neighbour is this node’s parent - the node it joined through |
child |
the neighbour joined through this node; this node is its parent |
sibling |
both share the same parent |
none_of_above |
heard, in radio range, but with no tree relationship - two routers that simply hear each other |
previous_child |
was this node’s child and no longer is: the device has reparented |
unknown |
the radio reported a value this plugin does not recognise |
The graph groups the six into four, because three of them answer the same question for a user - is this the tree, or just radio range?
| Graph group | Holds | Drawn |
|---|---|---|
| Parent / child | parent, child |
on by default. parent and child are one relationship seen from its two ends, so they collapse into one group |
| Sibling | sibling |
on by default |
| Other relationships | none_of_above, previous_child |
off by default |
| Unknown relationship | unknown |
on by default |
These four are the Relationship checkboxes on the page, and “by default” means how they are set when the page opens - see Filters and what they act on. Nothing is remembered between visits: every control is back at its default each time the page is opened.
none_of_above is the most common value on a healthy mesh of mains-powered devices and it is not a
fault - it is two routers that hear each other without one having joined through the other. It is
off by default because on a large network it can double the number of edges without changing the
conclusion. Switch it on when you are looking for redundancy: those are the links the network
could fall back on.
previous_child is worth switching on when a battery device has moved: it is the trace left behind
in the former parent’s table.
A neighbour row is a measurement made by the node that reported it. “A reports B at LQI 204” is a statement about A’s receiver, not about the pair. The two ends of the same radio link routinely disagree, and the report carries both rows separately.
The graph collapses the two rows into one drawn link, and keeps both readings:
This asymmetry is one of the more useful things in the report: a device that hears the mesh well but is not heard back will drop commands while looking perfectly healthy in a signal-strength reading.
LQI is a 0-255 link quality estimate, described in Network section > Terminology. It is not an RSSI and is not a dBm: a neighbour table carries LQI only. The graph bands it as:
| Band | LQI | Colour |
|---|---|---|
| Poor | 0-100 | red |
| Fair | 101-174 | blue |
| Good | 175-255 | green |
| No reading | neither end measured one | grey |
0 is a reading - it means heard at LQI 0. “No reading” is a different statement and is drawn
differently.
This is the distinction that causes the most confusion, so it is worth stating plainly:
A neighbour link is radio adjacency: these two nodes can hear each other. A route is a forwarding decision: to reach that destination, this node hands the frame to that neighbour next.
One is the map of the roads, the other is the signposting.
| Neighbours | Routes | |
|---|---|---|
| What a row means | “I hear this node, at this LQI” | “For this destination, my next hop is this node” |
| Direction | a measurement by the reporter | a forwarding decision by the reporter |
| Has an LQI | yes | no - a routing row carries no quality at all |
| Has a relationship | yes | no |
| Symmetric | usually, but not always | no: the return path can be different |
| Survives a reboot | rebuilt as the radio hears traffic | discovered on demand, ages out |

In Routing tables mode the graph draws one arrow per (reporter, next hop) pair, not one per
routing row: a router typically reaches a dozen destinations through the same neighbour, and a dozen
parallel arrows say nothing one does not. Click an arrow and the panel lists the destinations it
carries.
Because routing rows carry no LQI and no relationship, the LQI window and the relationship filters are greyed out in this mode - they have nothing to act on.
What to look for in the routing view:
next hop == destination is a direct, one-hop route and is common and healthy.active, discovery_underway, discovery_failed, inactive,
validation_underway. A discovery_failed row is a genuine finding. Note that zigpy filters
inactive rows out before the plugin ever sees them, so a route you remember may simply not be in
the report.many-to-one marks a route created by a many-to-one route request - the concentrator
mechanism a coordinator uses so that every node has a route to it without each one discovering
one (Silicon Labs - Zigbee Routing Concepts). route record required is its
companion: the node must send a route record so the concentrator learns the return path.memory constrained is a router that cannot store many routes - typically a small-memory
device that will force route discovery more often.A link in the neighbour view with no route over it is perfectly normal: no traffic has needed that path. A route over a pair that is not in the neighbour view is more interesting - usually it means one of the two did not answer the scan.
Every neighbour row carries a depth field. In the Zigbee specification (§2.4.4.3.2) it is the
neighbour’s tree depth - its distance from the coordinator in the association tree, 0 for the
coordinator, capped at 15 (nwkMaxDepth). Read that way, every node that describes device X should
give X the same depth.
In practice it does not work like that, and the field is the one part of a report that is routinely misread. What follows is measured on the network in the screenshots above - a ZNP coordinator and 62 devices, of which 30 could be asked for a neighbour table and 28 were reached - over the eight scans of a single day, of between 380 and 460 neighbour rows each.
Each radio behaves in one of three ways, and it behaves the same way in every scan - this is a fixed trait of the device’s stack, not a reading that moves with the mesh:
| Class | What the reporter’s depth column looks like | Share of the measured network |
|---|---|---|
| No depth | 15 on every row, except the coordinator row, which is always 0 |
9 of the 25 nodes that answered |
| Constant stamp | one value on every row whatever the neighbour - almost certainly the reporter’s own depth, stamped on each entry | 14 of 25, the coordinator among them |
| Genuine per-neighbour | more than one real value across the table | 2 of 25 |
The proof is that reporters contradict each other. In the scan measured here, 50 nodes are given a depth by somebody, and 23 of them are given two or three different ones. Two routers in the screenshots above, Switch Buanderie and Led Télé, are each depth 1 to one neighbour and depth 2 to another; a node cannot be both, so the number is not a property of that node.
Over the 462 rows of one scan:
| Value | Rows | |
|---|---|---|
15 |
319 (69%) | legal, but used as a sentinel by most stacks here |
255 |
22 (5%) | out of range; the plugin reports it as unknown, and the graph shows no depth at all |
0-14 |
121 (26%) | plausible - but most come from a constant stamp |
| of which, from a reporter that varies depth per neighbour | 34 (7%) | the rows actually worth reading |
The two reporters that do vary their depth are right about the part of the tree they are in:
So where the column moves at all, the parent and child rows hold up. The rest of the column does not, and nothing in a row says which kind of row you are holding.
0 is always the coordinator. Every stack gets that one right, and it carries no
information.parent and child rows of a genuine
reporter are the only part of the tree the scan can actually prove. The Associated Devices table
gives router→device edges but cannot rank them - on the measured network thirteen routers
declared twenty-five devices between them, which is thirteen separate stars with nothing placing
the routers themselves.For the question people actually ask of this field - how far is this device from the coordinator? - use hop distance over the links the scan reports, not depth. On the measured network that gives one unambiguous answer per node: the coordinator, 25 nodes one hop away and 25 two hops away. The eleven it cannot place are the honest part of the answer - nine battery devices no router reported hearing, a router that timed out, and one node the scan never reached - where depth offers 50 nodes with contradictory claims and no way to tell which claim to keep.
The graph already draws that: the concentric layout rings nodes by how connected they are, and breadthfirst roots the graph at the coordinator and lays out levels away from it. Both are computed from the links in front of you, and neither reads the depth column.
In short: do not lay a tree out with
depth, and do not colour nodes by it. Read it in the node panel, in the context of the whole column it came from, and treat a disagreement between two reporters as the finding it is rather than as noise.
The three tables say what each node knows. The question most people actually bring to a mesh diagram is the one that spans them: how does this device reach the coordinator, and what does it go through on the way? The Routes for control answers it.
Nothing is worked out until you pick a device, and the direction then follows from what you picked, because the question is not symmetrical:

Every route the coordinator has to the devices around it, drawn at once over the mesh underneath: solid purple where the radios stated the hop, dashed purple where this page worked it out. It is a star over the whole mesh, which is why the page does not open on it - and clearing the field takes it off again.

A battery motion sensor two hops from the controller, by way of the plug that declares it as one of its associated devices. Both hops are dashed - no router on the way had a settled routing row for the coordinator, so the whole route is the LQI estimate, and the note under it says why: this mesh uses many-to-one routing towards the coordinator, and those route records are held by the concentrator, where a topology scan cannot read them.
Clicking a device in the table, or the button in its node panel, switches to that device’s route.
Nothing extra is asked of the mesh: the route is assembled from the scan already on screen.
active. discovery_underway, validation_underway and
inactive are not decisions the radio has committed to, and treating one as a fact would
present a route that may never exist as an observation.cost = min(7, 1/p⁴),
with p estimated from the LQI. The graph takes the worse of a link’s two directions, as a
stack’s own link status exchange does, and runs Dijkstra. That is the rule route discovery is
supposed to follow - which, as the measurements below show, is not the same as what the radios
in a given house were seen to do.Every hop is labelled with which of the two produced it, and that distinction is the point:
| On the graph | In the panel | Meaning |
|---|---|---|
| solid purple | routing table, direct neighbour, through its parent |
the radios stated this hop |
| dashed purple | estimated |
this page worked the hop out |
A computed route is not an observed one. This is worth being blunt about, and the page prints it under every route it draws:
1/p⁴ has very little to say
between fine and hopeless. Read a dashed hop as about here, not as the path.Read a computed route as the most likely path, useful for “what is this slow device going through, and does that look sensible?” - and not as a packet capture.
The numbers from the scan in the screenshots, which is an ordinary house rather than a difficult network, say what to expect:
| Of the 61 devices beside the coordinator | |
|---|---|
| the coordinator reaches, every hop read from a table | 31 |
| the coordinator reaches, wholly estimated | 19 - 38 estimated hops in all |
| nothing in the scan connects | 11 - battery devices no router reported, and two dead nodes |
| whose route out to the coordinator is read from the tables | 25 |
| whose route out is wholly estimated | 25 |
| Routers carrying a many-to-one row for the coordinator | 17, and 23 rows ask for a route record |
The direction matters, and the asymmetry in that table is the many-to-one mechanism showing through: the coordinator mostly knows how to reach a device, while the device’s own way back lives in a route record the scan cannot read. A route out to the coordinator is therefore the one most likely to be drawn entirely dashed, which is why the page prints that note under it.
On the name. This view is called Associated devices in the WebUI, after the table it draws: for each router, the devices attached to it.
The words parent and child are the Zigbee specification’s own and are still used on this page, but for something else - the
parent/childvalue a neighbour row reports, under The relationships. That is a different table, gathered by a different request, and this view used to carry the same name, which is why it no longer does. For an end device the two coincide in practice: the router it is associated with is its parent, and holds its frames for it.
It does not come from a Mgmt_* table. It comes from the extended address request:
NWK_addr_req (or IEEE_addr_req) with RequestType 0x01, which a coordinator or a router answers
with NumAssocDev, StartIndex and NWKAddrAssocDevList - the list of devices associated to it.
Three things follow from that:
It is the only table that places a sleepy end device in the network.
A battery sensor answers no scan: it is asleep, it has no neighbour table, and asking it costs a timeout. In the neighbour and routing views it is either absent or an isolated dot. But the router it is attached to knows about it and answers for it - so in this view a Lumi/Aqara sensor, a door contact, a remote or a valve appears under the router it is actually attached to.
That is the question most people are really asking of a mesh diagram: which repeater is this sensor going through, and is that the one I expected?
It is also the fastest-changing part of a mesh. A battery device reparents - attaches itself to another router - whenever the one it is on goes away or a better one appears, and that happens far more often than the mains backbone changes shape. The page therefore offers this table as a collection of its own - see Refreshing the associated devices alone.

The associated devices view of the same scan, whole, in the breadthfirst layout: twelve routers along the top and the twenty-four devices they declare along the bottom, each link drawn as one router → device line. The counters read 24/24 association links, and the line under the graph counts the one row that names a node this scan does not list. Nothing in this table places the routers themselves, which is why it reads as a row of routers with their devices under them rather than as one hierarchy under the coordinator. Zoom in, or click a node, to read the names.
This is the question this table attracts most, so it is worth answering plainly: no, the associated device list is not a record of where or how you paired a device. It is where that device is attached now.
What actually happens when a device joins:
Three consequences:
0xFFFC - every router, plus the coordinator - so in normal use
you are not pairing “on” a particular router at all. Standing next to one biases the device’s
choice, because that router’s beacon is the strongest; it does not dictate it.So read an entry as “this is where the device is attached as of this scan”, and a change between two scans as the device having moved - never as a pairing you did or did not do correctly.
When two parents both declare the same child, the graph draws both edges in red and flags the device.
That is not a malformed report. It is a device that has reparented, leaving a stale entry behind in its previous parent’s table. Seeing it is half the reason to collect this table at all: it tells you a device has moved, and which way.
{node: [children]} - so a
router that was asked for its associated devices and timed out reads exactly like a router with none.
On a standalone tree collection there is no such ambiguity: the per-node scan status then
reports that collection’s own outcome.The Layout control chooses how the nodes are positioned. It changes nothing about the data - the same nodes and the same links, arranged differently. Changing it recomputes the positions; the Recompute layout button re-runs the current one, which is useful after dragging nodes around.
| Layout | What it does | Best for |
|---|---|---|
| cose (default) | Force-directed: links behave like springs, nodes repel each other, and the graph settles into its natural shape | The general-purpose view, and the one to start from. Clusters emerge on their own, so a group of devices that only hangs together through one router becomes visible as a lobe with a thin neck - which is exactly the fragile part of a mesh |
| concentric | Rings around the coordinator, placed by how many links each node has | Finding the hubs. The most connected nodes land in the inner rings, the leaves on the outside. Good for answering “which repeaters is this network actually leaning on?” - but on a backbone where the routers all have a similar number of links, those inner rings hold one or two nodes each and the middle of the picture reads as a column rather than as rings |
| breadthfirst | A hierarchy, rooted at the coordinator, level by level | The associated devices view - it is what that view is meant to be read in - and for seeing hop distance from the coordinator at a glance. It is the least useful on a densely meshed backbone, where it has to draw a hierarchy that does not really exist |
| circle | Every node on one circle, in order | Small networks, and auditing a single node. Nothing is hidden behind anything else, and every link is a visible chord. It becomes unreadable past a few dozen nodes |
| grid | Regular rows and columns | Inventory rather than topology. Every node occupies the same amount of space, so it is the easiest layout in which to confirm that a particular device is present, or to spot the isolated ones |
The neighbour tables of the same scan in cose, which lets the shape settle on its own, with Hide nodes with no visible link switched on so the layout has the canvas to itself:

The routers in the middle are the backbone, every leaf around them hangs off one of those, and the thickness of the knot in the centre is itself the finding: on this network most routers hear most other routers.
Practical sequence that works well:
The graph can be zoomed, panned, dragged and put fullscreen, and Focus a node (by name or network id) centres the graph on one device and fades the rest - the fastest way to answer a question about one specific device on a large mesh.
Every control on the page, what it acts on, and where it starts:
| Control | Acts on | When the page opens | Notes |
|---|---|---|---|
| Focus a node | the view | nothing focused | centres the graph on one device and fades the rest |
| Links to draw | which table is drawn | Neighbour tables | Neighbour tables / Routing tables / Both / Associated devices. Greyed out for a table the scan does not carry, and a scan that carries only the tree opens on the tree |
| Layout | positions only | cose | see Layouts |
| Routes for | the computed route | empty, so no route is worked out | pick the coordinator for how it reaches every device, or any device for its route out; clearing the field takes the route off the graph again - see Computed routes |
| Relationship: Parent / child | neighbour links only | on | see The relationships |
| Relationship: Sibling | neighbour links only | on | |
| Relationship: Other relationships | neighbour links only | off | none_of_above and previous_child; they can double the number of edges on a large mesh |
| Relationship: Unknown relationship | neighbour links only | on | |
| LQI window | neighbour links only | 0 - 255, so nothing is excluded | both bounds inclusive. A link neither end measured has no LQI, so it is outside every window - it is not hidden by narrowing the range, it is simply not a value |
| Hide end devices | nodes | off | removes the leaves to leave the backbone |
| Hide nodes with no visible link | nodes | off | hides whatever the current filters have left isolated. Useful after narrowing the LQI window |
| Device icons | drawing only | on | turns the per-device icons on or off; nothing is added or removed |
The relationship filters and the LQI window are greyed out in the routing and associated-devices views, because neither a routing row nor an association row carries a relationship or an LQI for them to act on.
Nothing is remembered between visits: the controls are back at the settings above every time the page is opened. Changing the selected scan does not reset them, so two scans can be compared under the same filters.
The counters under the graph always read visible / total, so a filter never hides the fact that it is hiding something.
Node colour is the role the node plays in the mesh: coordinator (purple), router (blue), end device (teal), unknown (grey).
Node icon is what the device is - a light, a plug, a switch, a sensor, a valve - resolved from the device’s own descriptors. A device that reported none is drawn as a plain shape.
Node border is how the node is doing, worst first:
| Border | Meaning |
|---|---|
| thick red | Not reachable - the plugin’s own health for this device |
| thick dashed orange | Timed out - it was asked for its tables and never answered. This is a finding, not a leaf with no neighbours |
| dashed grey | Not scanned - it has a neighbour table in principle, but this scan never reached it |
| amber | Not seen recently - no traffic from this device in the last 24 hours |
| plain | fine |
Being able to tell timed out from has no neighbours is the single most useful thing this page does that the older charts could not: before, both were drawn as a dot with no links.
Click anything. A node opens a panel with its identity (network id, IEEE address, model, type, health, battery, LQI/RSSI), its scan status, what it is attached to and what is attached to it, and every neighbour and routing row that mentions it. A link opens the two LQI readings. An arrow opens the destinations it carries.
The health, model, LQI, RSSI and battery shown for a node are that device’s values now, not its values at the time of the scan. Only the tables are historical.
The coverage banner above the graph is not decoration. When it says “4 of 32 nodes answered this scan”, the picture below is a picture of those 4 nodes. See the next section.
A line under the graph counts the rows that could not be drawn, when there are any: “1 report row(s) name a node this scan does not list, and were not drawn”. A table can name a network address the scan has no node for - a device that has since left, an address reused, or in the scan used throughout this page a device the coordinator still declares as attached to it. Those rows are counted rather than invented into a node, and the count is worth a glance: several of them at once usually means the report and the device list have drifted apart.
The Topology scan card at the top right of the Mesh page has two buttons:
/req-topologie the Topology page has always used.Both answer immediately: the scan itself runs in the background and takes minutes. A scan appears in the list only once it has finished - there is no half-written report. While one runs, the WebUI footer reports a topology scan in progress, for both halves of it.
A scan can also start by itself, periodically, if ZigpyAutoTopology is enabled (it is off by
default - see Settings).
On a zigpy coordinator (ZNP, EZSP/Bellows, deCONZ, BLZ) the first half is run by zigpy itself:
Mgmt_Lqi_req is sent with a start index, the answer says how many
entries exist in total, and the request is repeated until the table is exhausted. Same for
Mgmt_Rtg_req.inactive routes are dropped before the plugin sees them.So the cost of a scan is roughly (number of routers) x (pages x (request + ~1.25 s)), twice - once for neighbours, once for routes. On a few dozen mains-powered devices that is minutes, not seconds.
When ZigpyTopologyAssociatedDevices is enabled, the plugin then collects the associated device table
itself, because zigpy does not:
Both halves are filed as one scan under one timestamp, which is the moment zigpy’s tables were read.
If the radio restarts mid-collection, the scan is filed with what it has - zigpy’s two tables and no associated devices section. An absent section is how the report says “this table was not collected”, which is a different statement from “collected and found nothing”.
Associated devices runs that second half on its own, without a full scan. One request per router instead of two paginated tables - much cheaper - and it refreshes exactly the part of the mesh that changes most often.
It is filed as a scan of its own, carrying the associated device table alone. Its neighbour and route counts are zero because they were never asked for, and the page labels it as such rather than drawing an empty mesh. It is refused while any other scan is in progress, and it needs a zigpy coordinator.
Reports are kept in the plugin’s reports folder (Zigpy-Topology-XX.json), newest last, and the
oldest is dropped once there are more than numTopologyReports of them (4 by default). Keeping
several is what makes two scans comparable - which is also why each scan must be a fresh reading and
not an accumulation.
This is the part of a topology report that is most often misread, so it is worth going through carefully.
A mains-powered device that is unplugged, or a bulb whose wall switch is off, is still in the plugin’s device list and is still asked. The scanner has no way to know it is gone until it fails to answer. What follows:
timeout - thick dashed orange border - and the coverage banner counts it. That is
the honest answer: asked, said nothing.zigpy’s tables are cumulative per device. Each scan replaces the rows of each device it successfully scanned, and leaves the rows of a device that failed exactly as they were. So:
timeout, with no links, which is
the correct reading.If you have just switched a lot of things off or on, restart the plugin before trusting a topology report as a snapshot of “now”. A plugin restart clears zigpy’s accumulated tables; a new scan then reports only what answered this time.
The practical rule: a report is reliable about the links it shows for nodes that answered this scan, and should be read with suspicion for every node the coverage banner counts as timed out.
A Zigbee bulb is a router. Turn it off at the wall, and you have not “turned off a light” - you have removed a relay from the backbone. On a mesh where a room hangs off one bulb, that is a hole, and everything behind it goes quiet or slows down while routes are rediscovered.
In the graph it reads as: one node timed out, a lobe of the network suddenly attached by a single thin link or not attached at all, and a handful of contested devices in the next collection.
This is the single most common cause of “my Zigbee network is unreliable in the evening”.
A sleepy end device is reported as “No neighbour table” (not_supported), not as a failure. It
is not counted against the scan’s coverage. It appears:
Not scanned means the node has a neighbour table in principle, but this scan did not get to it - a scan cut short by a radio restart, or a device the scanner skipped. It is a gap in the picture, not a statement about the device.
answered / requested, where requested counts only the nodes that could have answered. A node
that cannot answer at all - an end device on a neighbour scan - is not counted against the scan.
When the banner says “4 of 32 nodes answered this scan”, the graph below is a picture of four nodes’ opinions about the network. Drawing conclusions about the other 28 from it is the mistake the banner exists to prevent.
All of these live in the WebUI under Settings, in the advanced section.
| Setting | Default | What it does |
|---|---|---|
ZigpyTopologyReport |
on | Use the zigpy topology report. With it off, /req-topologie falls back to the legacy NetworkMap scan |
ZigpyAutoTopology |
off | Let zigpy run its own periodic scan (every 12 hours). Only effective when a zigpy persistent database is enabled |
ZigpyTopologyReportAutoBackup |
off | Keep the report of a scan nobody asked for. With it off, only a scan you requested is filed |
ZigpyTopologyAssociatedDevices |
off | Collect the associated device table as the second half of a full scan. Not required for the standalone Associated devices button - asking for the table there is the request |
numTopologyReports |
4 | How many reports are kept before the oldest is dropped |
TopologyV2 |
on | The non-zigpy (native ZiGate) collection path |
On a native ZiGate, ZigpyTopologyReport and ZigpyTopologyAssociatedDevices are forced off: the
TopologyV2 scan collects all three tables itself, on every scan.
Zigbee:
Mgmt_Lqi_rsp §2.4.4.3.2, Mgmt_Rtg_rsp §2.4.4.3.3, and the extended address
request NWK_addr_req/IEEE_addr_req with RequestType 0x01Plugin:
docs/TopologyScanAPI.md in the plugin repository - the /topology-scan REST contract this page
is drawn from, for anyone building their own toolingLook at the other pages of the plugin Web interface.