1038 lines
42 KiB
QML
1038 lines
42 KiB
QML
import QtQuick 2.4
|
|
import QtQuick.Controls 2.1
|
|
import QtQuick.Layouts 1.1
|
|
import "qrc:/ui/components"
|
|
import Nymea 1.0
|
|
|
|
Page {
|
|
id: root
|
|
|
|
header: NymeaHeader {
|
|
text: qsTr("ZigBee network map")
|
|
backButtonVisible: true
|
|
onBackPressed: pageStack.pop()
|
|
HeaderButton {
|
|
imageSource: "/ui/images/help.svg"
|
|
text: qsTr("Help")
|
|
onClicked: {
|
|
var popup = zigbeeHelpDialog.createObject(app)
|
|
popup.open()
|
|
}
|
|
}
|
|
}
|
|
|
|
property ZigbeeManager zigbeeManager: null
|
|
property ZigbeeNetwork network: null
|
|
|
|
readonly property int nodeDistance: Style.iconSize * 2
|
|
readonly property int nodeSize: Style.iconSize + Style.margins
|
|
property double scale: 1
|
|
readonly property double minScale: 0.5
|
|
readonly property double maxScale: 1.5
|
|
|
|
|
|
Component.onCompleted: {
|
|
zigbeeManager.refreshNeighborTables(network.networkUuid)
|
|
|
|
reload();
|
|
for (var i = 0; i < network.nodes.count; i++) {
|
|
network.nodes.get(i).neighborsChanged.connect(root.reloadDelayed)
|
|
}
|
|
}
|
|
Component.onDestruction: {
|
|
for (var i = 0; i < network.nodes.count; i++) {
|
|
network.nodes.get(i).neighborsChanged.disconnect(root.reloadDelayed)
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.network.nodes
|
|
onNodeAdded: {
|
|
root.reload()
|
|
node.neighborsChanged.connect(root.reloadDelayed);
|
|
}
|
|
}
|
|
|
|
function reload() {
|
|
print("Reloading network map")
|
|
generateNodeList();
|
|
generateEdges();
|
|
canvas.requestPaint()
|
|
// print("repainting", flickable.contentX, flickable.contentY)
|
|
if (flickable.contentX == 0 && flickable.contentY == 0) {
|
|
flickable.contentX = (flickable.contentWidth - flickable.width) / 2
|
|
flickable.contentY = (flickable.contentHeight - flickable.height) / 2
|
|
}
|
|
}
|
|
|
|
function reloadDelayed() {
|
|
reloadTimer.start()
|
|
}
|
|
|
|
Timer {
|
|
id: reloadTimer
|
|
interval: 500
|
|
repeat: false
|
|
running: false
|
|
onTriggered: reload()
|
|
}
|
|
|
|
QtObject {
|
|
id: d
|
|
property var nodeItems: ({})
|
|
property var edges: ({})
|
|
|
|
property int selectedNodeAddress: -1
|
|
readonly property var selectedNodeItem: nodeItems[selectedNodeAddress]
|
|
|
|
readonly property ZigbeeNode selectedNode: selectedNodeAddress >= 0 ? network.nodes.getNodeByNetworkAddress(selectedNodeAddress) : null
|
|
|
|
property int minX: 0
|
|
property int minY: 0
|
|
property int maxX: 0
|
|
property int maxY: 0
|
|
property int size: 0
|
|
|
|
function adjustSize(x, y) {
|
|
minX = Math.min(minX, x)
|
|
minY = Math.min(minY, y)
|
|
maxX = Math.max(maxX, x)
|
|
maxY = Math.max(maxY, y)
|
|
var minWidth = Math.max(-minX, maxX) * 2 * root.maxScale
|
|
var minHeight = Math.max(-minY, maxY) * 2 * root.maxScale
|
|
size = Math.max(minWidth, minHeight) + root.nodeSize + Style.hugeMargins * 2
|
|
}
|
|
}
|
|
|
|
function generateNodeList() {
|
|
var nodeItems = ({})
|
|
var coordinator = {}
|
|
var routers = []
|
|
var endDevices = []
|
|
for (var i = 0; i < root.network.nodes.count; i++) {
|
|
var node = root.network.nodes.get(i);
|
|
switch (node.type) {
|
|
case ZigbeeNode.ZigbeeNodeTypeRouter:
|
|
routers.push(node)
|
|
break;
|
|
case ZigbeeNode.ZigbeeNodeTypeEndDevice:
|
|
endDevices.push(node);
|
|
break;
|
|
case ZigbeeNode.ZigbeeNodeTypeCoordinator:
|
|
coordinator = node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var startAngle = -90
|
|
|
|
var routersCircumference = Math.max(5, routers.length) * (root.nodeSize + root.nodeDistance)// * root.scale
|
|
var distanceFromCenter = routersCircumference / 2 / Math.PI
|
|
|
|
routers.unshift(coordinator)
|
|
|
|
var handledEndDevices = []
|
|
|
|
var angle = 360 / routers.length;
|
|
for (var i = 0; i < routers.length; i++) {
|
|
var router = routers[i]
|
|
var nodeAngle = startAngle + angle * i;
|
|
var x = distanceFromCenter * Math.cos(nodeAngle * Math.PI / 180)
|
|
var y = distanceFromCenter * Math.sin(nodeAngle * Math.PI / 180)
|
|
nodeItems[router.networkAddress] = createNodeItem(router, x, y, nodeAngle);
|
|
|
|
|
|
var neighborCounter = 0;
|
|
for (var j = 0; j < router.neighbors.length; j++) {
|
|
var neighborNode = root.network.nodes.getNodeByNetworkAddress(router.neighbors[j].networkAddress)
|
|
if (!neighborNode) {
|
|
continue
|
|
}
|
|
|
|
if (neighborNode.type == ZigbeeNode.ZigbeeNodeTypeEndDevice) {
|
|
if (handledEndDevices.indexOf(neighborNode.networkAddress) >= 0) {
|
|
continue;
|
|
}
|
|
handledEndDevices.push(neighborNode.networkAddress)
|
|
|
|
var neighborAngle = nodeAngle + neighborCounter * 8
|
|
var neighborDistance = (distanceFromCenter + (root.nodeDistance + root.nodeSize)) + neighborCounter * root.nodeDistance * .5
|
|
|
|
x = neighborDistance * Math.cos(neighborAngle * Math.PI / 180)
|
|
y = neighborDistance * Math.sin(neighborAngle * Math.PI / 180)
|
|
nodeItems[neighborNode.networkAddress] = createNodeItem(neighborNode, x, y, angle)
|
|
|
|
neighborCounter++
|
|
}
|
|
}
|
|
}
|
|
|
|
var unconnectedNodes = []
|
|
for (var i = 0; i < network.nodes.count; i++) {
|
|
var node = network.nodes.get(i)
|
|
if (node.type == ZigbeeNode.ZigbeeNodeTypeEndDevice && handledEndDevices.indexOf(node.networkAddress) < 0) {
|
|
// print("Adding unconnected node:","0x" + node.networkAddress.toString(16))
|
|
unconnectedNodes.push(node)
|
|
}
|
|
}
|
|
var cellWidth = root.nodeSize * 2
|
|
var cellHeight = root.nodeSize * 2
|
|
var maxColumns = (root.width - Style.bigMargins * 2) / cellWidth
|
|
var columns = Math.min(unconnectedNodes.length, maxColumns)
|
|
var rowWidth = columns * cellWidth
|
|
var rows = Math.floor(unconnectedNodes.length / columns)
|
|
for (var i = 0; i < unconnectedNodes.length; i++) {
|
|
var node = unconnectedNodes[i]
|
|
var column = i % columns;
|
|
var row = Math.floor(i / columns)
|
|
var x = nodeItems[coordinator.networkAddress].x + (column + 0.5) * cellWidth - rowWidth / 2
|
|
var y = nodeItems[coordinator.networkAddress].y - root.nodeSize * (5 + rows) + cellHeight * row
|
|
nodeItems[node.networkAddress] = createNodeItem(node, x, y, 0)
|
|
}
|
|
|
|
for (var networkAddress in d.nodeItems) {
|
|
if (!nodeItems.hasOwnProperty(networkAddress)) {
|
|
d.nodeItems[networkAddress].image.destroy();
|
|
}
|
|
}
|
|
|
|
d.nodeItems = nodeItems
|
|
}
|
|
|
|
function createNodeItem(node, x, y, angle) {
|
|
d.adjustSize(x, y)
|
|
|
|
if (d.nodeItems.hasOwnProperty(node.networkAddress)) {
|
|
d.nodeItems[node.networkAddress].x = x;
|
|
d.nodeItems[node.networkAddress].y = y;
|
|
d.nodeItems[node.networkAddress].image.x = Qt.binding(function() { return x * root.scale + (canvas.width - Style.iconSize * root.scale) / 2});
|
|
d.nodeItems[node.networkAddress].image.y = Qt.binding(function() { return y * root.scale + (canvas.height - Style.iconSize * root.scale) / 2});
|
|
|
|
d.nodeItems[node.networkAddress].angle = angle;
|
|
return d.nodeItems[node.networkAddress]
|
|
}
|
|
|
|
var icon = "/ui/images/zigbee.svg"
|
|
var thing = null
|
|
if (node.networkAddress === 0) {
|
|
icon = "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle)
|
|
} else {
|
|
for (var i = 0; i < engine.thingManager.things.count; i++) {
|
|
var t = engine.thingManager.things.get(i)
|
|
// print("checking thing", t.name)
|
|
var param = t.paramByName("ieeeAddress")
|
|
if (param && param.value === node.ieeeAddress) {
|
|
thing = t;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (thing) {
|
|
icon = app.interfacesToIcon(thing.thingClass.interfaces)
|
|
}
|
|
|
|
var nodeItem = {
|
|
node: node,
|
|
x: x,
|
|
y: y,
|
|
edges: [],
|
|
image: imageComponent.createObject(canvas, {
|
|
x: Qt.binding(function() { return x * root.scale + (canvas.width - Style.iconSize * root.scale) / 2}),
|
|
y: Qt.binding(function() { return y * root.scale + (canvas.height - Style.iconSize * root.scale) / 2}),
|
|
size: Qt.binding(function() {return Style.iconSize * root.scale}),
|
|
name: icon,
|
|
color: Style.accentColor
|
|
}),
|
|
thing: thing
|
|
|
|
}
|
|
return nodeItem
|
|
}
|
|
|
|
function generateEdges() {
|
|
var edges = ({})
|
|
for (var networkAddress in d.nodeItems) {
|
|
var fromNodeItem = d.nodeItems[networkAddress]
|
|
var fromAddress = fromNodeItem.node.networkAddress
|
|
for (var i = 0; i < fromNodeItem.node.neighbors.length; i++) {
|
|
var neighbor = fromNodeItem.node.neighbors[i]
|
|
// print("have neighbor", neighbor.networkAddress)
|
|
var toAddress = neighbor.networkAddress
|
|
if (!d.nodeItems.hasOwnProperty(toAddress)) {
|
|
continue;
|
|
}
|
|
var toNodeItem = d.nodeItems[toAddress]
|
|
|
|
var edgeKey = fromAddress < toAddress ? fromAddress + "+" + toAddress : toAddress + "+" + fromAddress;
|
|
if (edges.hasOwnProperty(edgeKey)) {
|
|
continue;
|
|
}
|
|
|
|
var fromLqi = neighbor.lqi
|
|
var toLqi = fromLqi
|
|
for (var j = 0; j < toNodeItem.node.neighbors.length; j++) {
|
|
if (toNodeItem.node.neighbors[j].networkAddress === fromAddress) {
|
|
toLqi = toNodeItem.node.neighbors[j].lqi
|
|
break;
|
|
}
|
|
}
|
|
|
|
edges[edgeKey] = {
|
|
fromNodeItem: fromNodeItem,
|
|
toNodeItem: toNodeItem,
|
|
fromLqi: fromLqi,
|
|
toLqi: toLqi
|
|
}
|
|
}
|
|
}
|
|
d.edges = edges
|
|
}
|
|
|
|
Component {
|
|
id: imageComponent
|
|
ColorIcon {
|
|
}
|
|
}
|
|
|
|
Flickable {
|
|
id: flickable
|
|
anchors.fill: parent
|
|
clip: true
|
|
|
|
contentWidth: canvas.width
|
|
contentHeight: canvas.height
|
|
|
|
Canvas {
|
|
id: canvas
|
|
width: Math.max(d.size, flickable.width)
|
|
height: Math.max(d.size, flickable.height)
|
|
clip: true
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d");
|
|
ctx.reset();
|
|
|
|
var center = { x: canvas.width / 2, y: canvas.height / 2 };
|
|
ctx.translate(center.x, center.y)
|
|
|
|
paintNodeList(ctx);
|
|
}
|
|
|
|
function paintNodeList(ctx) {
|
|
for (var edgeKey in d.edges) {
|
|
paintEdge(ctx, d.edges[edgeKey], false)
|
|
}
|
|
|
|
if (d.selectedNodeItem) {
|
|
paintRoutes(ctx, d.selectedNodeItem)
|
|
}
|
|
for (var networkAddress in d.nodeItems) {
|
|
paintNode(ctx, d.nodeItems[networkAddress])
|
|
}
|
|
}
|
|
|
|
function paintRoutes(ctx, nodeItem) {
|
|
var node = nodeItem.node
|
|
var nextHop = -1
|
|
if (node.type === ZigbeeNode.ZigbeeNodeTypeRouter) {
|
|
for (var i = 0; i < node.routes.length; i++) {
|
|
if (node.routes[i].destinationAddress === 0) {
|
|
nextHop = node.routes[i].nextHopAddress
|
|
paintRoute(ctx, nodeItem, nextHop);
|
|
}
|
|
}
|
|
} else if (node.type === ZigbeeNode.ZigbeeNodeTypeEndDevice) {
|
|
for (var i = 0; i < network.nodes.count; i++) {
|
|
for (var j = 0; j < network.nodes.get(i).neighbors.length; j++) {
|
|
if (network.nodes.get(i).neighbors[j].networkAddress === node.networkAddress) {
|
|
nextHop = network.nodes.get(i).networkAddress
|
|
paintRoute(ctx, nodeItem, nextHop);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function paintRoute(ctx, fromNodeItem, nextHopAddress) {
|
|
if (!d.nodeItems.hasOwnProperty(nextHopAddress)) {
|
|
return;
|
|
}
|
|
var toNodeItem = d.nodeItems[nextHopAddress]
|
|
var fromAddress = fromNodeItem.node.networkAddress
|
|
|
|
var edgeKey = fromAddress < nextHopAddress ? fromAddress + "+" + nextHopAddress : nextHopAddress + "+" + fromAddress
|
|
paintEdge(ctx, d.edges[edgeKey], true)
|
|
|
|
paintRoutes(ctx, toNodeItem)
|
|
|
|
}
|
|
|
|
function paintNode(ctx, nodeItem) {
|
|
ctx.save()
|
|
ctx.beginPath();
|
|
ctx.fillStyle = nodeItem.node.networkAddress === d.selectedNodeAddress ? Style.tileOverlayColor : Style.tileBackgroundColor
|
|
ctx.strokeStyle = nodeItem.node.networkAddress === d.selectedNodeAddress ? Style.accentColor : Style.tileBackgroundColor
|
|
ctx.arc(nodeItem.x * root.scale, nodeItem.y * root.scale, root.scale * root.nodeSize / 2, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
// ctx.stroke();
|
|
ctx.fillStyle = Style.foregroundColor
|
|
ctx.font = "" + Style.extraSmallFont.pixelSize + "px Ubuntu";
|
|
var text = ""
|
|
if (nodeItem.thing) {
|
|
text = nodeItem.thing.name
|
|
} else {
|
|
text = nodeItem.node.model
|
|
}
|
|
if (text.length > 10) {
|
|
text = text.substring(0, 9) + "…"
|
|
}
|
|
|
|
var textSize = ctx.measureText(text)
|
|
// ctx.fillText(text, scale * (nodeItem.x ), scale * (nodeItem.y ))
|
|
ctx.fillText(text, nodeItem.x * root.scale - (textSize.width / 2), nodeItem.y * root.scale + (root.scale * root.nodeSize / 2 + Style.extraSmallFont.pixelSize))
|
|
|
|
ctx.closePath();
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
function paintEdge(ctx, edge, forceSelection) {
|
|
ctx.save();
|
|
var haveSelection = d.selectedNodeItem !== undefined
|
|
var fromSelected = edge.fromNodeItem === d.selectedNodeItem
|
|
var toSelected = edge.toNodeItem === d.selectedNodeItem
|
|
var fromPercent = 1.0 * edge.fromLqi / 255;
|
|
var fromColor = Qt.rgba(
|
|
Style.red.r + fromPercent * (Style.green.r - Style.red.r),
|
|
Style.red.g + fromPercent * (Style.green.g - Style.red.g),
|
|
Style.red.b + fromPercent * (Style.green.b - Style.red.b),
|
|
haveSelection && !fromSelected && !forceSelection ? .2 : 1
|
|
)
|
|
var toPercent = 1.0 * edge.toLqi / 255;
|
|
var toColor = Qt.rgba(
|
|
Style.red.r + toPercent * (Style.green.r - Style.red.r),
|
|
Style.red.g + toPercent * (Style.green.g - Style.red.g),
|
|
Style.red.b + toPercent * (Style.green.b - Style.red.b),
|
|
haveSelection && !toSelected && !forceSelection ? .2 : 1
|
|
)
|
|
var fromX = root.scale * edge.fromNodeItem.x
|
|
var fromY = root.scale * edge.fromNodeItem.y
|
|
var toX = root.scale * edge.toNodeItem.x
|
|
var toY = root.scale * edge.toNodeItem.y
|
|
|
|
var gradient = ctx.createLinearGradient(fromX, fromY, toX, toY);
|
|
gradient.addColorStop(0, fromColor);
|
|
gradient.addColorStop(1, toColor)
|
|
ctx.lineWidth = forceSelection ? 3 : fromSelected || toSelected ? 2 : 1
|
|
ctx.strokeStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.moveTo(fromX, fromY)
|
|
ctx.lineTo(toX, toY)
|
|
ctx.stroke();
|
|
ctx.closePath()
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
|
|
PinchArea {
|
|
anchors.fill: parent
|
|
property double startScale: 0
|
|
onPinchStarted: {
|
|
startScale = root.scale
|
|
print("pinch started", startScale)
|
|
}
|
|
|
|
onPinchUpdated: {
|
|
print("pinch updated:", pinch.scale)
|
|
var scaleDiff = pinch.scale - 1
|
|
root.scale = Math.min(root.maxScale, Math.max(root.minScale, startScale + scaleDiff))
|
|
root.reload()
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
|
|
onClicked: {
|
|
print("clicked:", mouseX, mouseY)
|
|
var translatedMouseX = (mouseX - canvas.width / 2)
|
|
var translatedMouseY = (mouseY - canvas.height / 2)
|
|
d.selectedNodeAddress = -1
|
|
for (var networkAddress in d.nodeItems) {
|
|
var nodeItem = d.nodeItems[networkAddress]
|
|
// print("nodeItem at:", root.scale * nodeItem.x, root.scale * nodeItem.y)
|
|
if (Math.abs(nodeItem.x * root.scale - translatedMouseX) < (root.scale * root.nodeSize / 2)
|
|
&& Math.abs(nodeItem.y * root.scale - translatedMouseY) < (root.scale * root.nodeSize / 2)) {
|
|
d.selectedNodeAddress = nodeItem.node.networkAddress;
|
|
print("selecting", nodeItem.node.networkAddress)
|
|
for (var j = 0; j < nodeItem.node.routes.length; j++) {
|
|
var route = nodeItem.node.routes[j]
|
|
print("route:", route.destinationAddress, "via", route.nextHopAddress)
|
|
}
|
|
}
|
|
}
|
|
|
|
canvas.requestPaint();
|
|
}
|
|
|
|
onWheel: {
|
|
if (wheel.modifiers & Qt.ControlModifier) {
|
|
root.scale = Math.min(root.maxScale, Math.max(root.minScale, root.scale + 1.0 * wheel.angleDelta.y / 1000))
|
|
root.reload()
|
|
} else {
|
|
wheel.accepted = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
BigTile {
|
|
id: infoTile
|
|
visible: d.selectedNodeAddress >= 0
|
|
property point selectedNodeItemPos: d.selectedNodeItem ? Qt.point(d.selectedNodeItem.x + flickable.contentWidth / 2 - flickable.contentX, d.selectedNodeItem.y + flickable.contentHeight / 2 - flickable.contentY) : Qt.point(0,0)
|
|
onSelectedNodeItemPosChanged: print("selected point:", selectedNodeItemPos, flickable.contentX)
|
|
x: selectedNodeItemPos.x < flickable.width / 2 ? flickable.width - width - Style.smallMargins : Style.smallMargins
|
|
y: selectedNodeItemPos.y < flickable.height / 2 ? flickable.height - height- Style.smallMargins : Style.smallMargins
|
|
Behavior on x { NumberAnimation { duration: Style.fastAnimationDuration; easing.type: Easing.InOutQuad } }
|
|
Behavior on y { NumberAnimation { duration: Style.fastAnimationDuration; easing.type: Easing.InOutQuad } }
|
|
|
|
width: 260
|
|
header: RowLayout {
|
|
width: parent.width - Style.smallMargins
|
|
spacing: Style.smallMargins
|
|
Label {
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
ThingsProxy {
|
|
id: selectedThingsProxy
|
|
engine: _engine
|
|
paramsFilter: {"ieeeAddress": d.selectedNode ? d.selectedNode.ieeeAddress : "---"}
|
|
}
|
|
|
|
text: d.selectedNodeAddress < 0
|
|
? ""
|
|
: d.selectedNodeAddress === 0
|
|
? Configuration.systemName
|
|
: selectedThingsProxy.count > 0
|
|
? selectedThingsProxy.get(0).name
|
|
: network.nodes.getNodeByNetworkAddress(d.selectedNode).model
|
|
}
|
|
|
|
ColorIcon {
|
|
size: Style.smallIconSize
|
|
name: {
|
|
if (!d.selectedNode) {
|
|
return "";
|
|
}
|
|
|
|
var signalStrength = 100.0 * d.selectedNode.lqi / 255
|
|
if (!d.selectedNode.reachable)
|
|
return "/ui/images/connections/nm-signal-00.svg"
|
|
if (signalStrength <= 25)
|
|
return "/ui/images/connections/nm-signal-25.svg"
|
|
if (signalStrength <= 50)
|
|
return "/ui/images/connections/nm-signal-50.svg"
|
|
if (signalStrength <= 75)
|
|
return "/ui/images/connections/nm-signal-75.svg"
|
|
if (signalStrength <= 100)
|
|
return "/ui/images/connections/nm-signal-100.svg"
|
|
}
|
|
}
|
|
}
|
|
|
|
contentItem: ColumnLayout {
|
|
width: infoTile.width
|
|
SelectionTabs {
|
|
id: infoSelectionTabs
|
|
Layout.fillWidth: true
|
|
color: Style.tileOverlayColor
|
|
selectionColor: Qt.tint(Style.tileOverlayColor, Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, 0.1))
|
|
model: ListModel {
|
|
ListElement {
|
|
text: qsTr("Device")
|
|
}
|
|
ListElement {
|
|
text: qsTr("Links")
|
|
}
|
|
ListElement {
|
|
text: qsTr("Routes")
|
|
}
|
|
}
|
|
}
|
|
|
|
GridLayout {
|
|
Layout.fillWidth: true
|
|
visible: infoSelectionTabs.currentIndex == 0
|
|
columns: 2
|
|
columnSpacing: Style.smallMargins
|
|
|
|
Label {
|
|
text: qsTr("Address:")
|
|
font: Style.smallFont
|
|
Layout.fillWidth: true
|
|
}
|
|
Label {
|
|
text: d.selectedNode ? "0x" + d.selectedNode.networkAddress.toString(16) : ""
|
|
font: Style.smallFont
|
|
horizontalAlignment: Text.AlignRight
|
|
Layout.fillWidth: true
|
|
}
|
|
Label {
|
|
text: qsTr("Model:")
|
|
font: Style.smallFont
|
|
Layout.fillWidth: true
|
|
}
|
|
Label {
|
|
text: d.selectedNode ? d.selectedNode.model : ""
|
|
font: Style.smallFont
|
|
horizontalAlignment: Text.AlignRight
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
Label {
|
|
text: qsTr("Manufacturer:")
|
|
font: Style.smallFont
|
|
Layout.fillWidth: true
|
|
}
|
|
Label {
|
|
text: d.selectedNode ? d.selectedNode.manufacturer : ""
|
|
font: Style.smallFont
|
|
horizontalAlignment: Text.AlignRight
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
Label {
|
|
text: qsTr("Last seen:")
|
|
font: Style.smallFont
|
|
Layout.fillWidth: true
|
|
}
|
|
Label {
|
|
text: d.selectedNode ? d.selectedNode.lastSeen.toLocaleString(Qt.locale(), Locale.ShortFormat) : ""
|
|
font: Style.smallFont
|
|
horizontalAlignment: Text.AlignRight
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
visible: infoSelectionTabs.currentIndex == 1
|
|
RowLayout {
|
|
Label {
|
|
text: qsTr("Neighbor")
|
|
font: Style.smallFont
|
|
Layout.fillWidth: true
|
|
}
|
|
ColorIcon {
|
|
size: Style.smallIconSize
|
|
name: "connections/nm-signal-50"
|
|
}
|
|
ColorIcon {
|
|
size: Style.smallIconSize
|
|
name: "zigbee/zigbee-coordinator"
|
|
}
|
|
Item {
|
|
Layout.preferredWidth: Style.smallIconSize + Style.smallMargins
|
|
Layout.fillHeight: true
|
|
ColorIcon {
|
|
anchors.centerIn: parent
|
|
size: Style.smallIconSize
|
|
name: "arrow-down"
|
|
}
|
|
}
|
|
|
|
ColorIcon {
|
|
size: Style.smallIconSize
|
|
name: "add"
|
|
}
|
|
}
|
|
ThinDivider {
|
|
color: Style.foregroundColor
|
|
}
|
|
|
|
ListView {
|
|
id: neighborTableListView
|
|
Layout.fillWidth: true
|
|
// spacing: app.margins
|
|
implicitHeight: Math.min(root.height / 4, count * Style.smallIconSize)
|
|
clip: true
|
|
model: d.selectedNode ? d.selectedNode.neighbors.length : 0
|
|
|
|
delegate: RowLayout {
|
|
id: neighborTableDelegate
|
|
width: neighborTableListView.width
|
|
property ZigbeeNodeNeighbor neighbor: d.selectedNode.neighbors[index]
|
|
property ZigbeeNode neighborNode: root.network.nodes.getNodeByNetworkAddress(neighbor.networkAddress)
|
|
property Thing neighborNodeThing: {
|
|
for (var i = 0; i < engine.thingManager.things.count; i++) {
|
|
var thing = engine.thingManager.things.get(i)
|
|
var param = thing.paramByName("ieeeAddress")
|
|
if (param && param.value == neighborNode.ieeeAddress) {
|
|
return thing
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
Label {
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
font: Style.smallFont
|
|
text: neighborTableDelegate.neighbor.networkAddress === 0
|
|
? Configuration.systemName
|
|
: neighborTableDelegate.neighborNodeThing
|
|
? neighborTableDelegate.neighborNodeThing.name
|
|
: neighborTableDelegate.neighborNode
|
|
? neighborTableDelegate.neighborNode.model
|
|
: "0x" + neighborTableDelegate.neighbor.networkAddress.toString(16)
|
|
color: neighborTableDelegate.neighborNode ? Style.foregroundColor : Style.red
|
|
}
|
|
Label {
|
|
text: (neighborTableDelegate.neighbor.lqi * 100 / 255).toFixed(0) + "%"
|
|
font: Style.smallFont
|
|
horizontalAlignment: Text.AlignRight
|
|
}
|
|
ColorIcon {
|
|
size: Style.smallIconSize
|
|
name: {
|
|
switch (neighborTableDelegate.neighbor.relationship) {
|
|
case ZigbeeNode.ZigbeeNodeRelationshipChild:
|
|
return "zigbee/zigbee-child"
|
|
case ZigbeeNode.ZigbeeNodeRelationshipParent:
|
|
return "zigbee/zigbee-parent"
|
|
case ZigbeeNode.ZigbeeNodeRelationshipSibling:
|
|
return "zigbee/zigbee-sibling"
|
|
case ZigbeeNode.ZigbeeNodeRelationshipPreviousChild:
|
|
return "zigbee/zigbee-previous-child"
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
Label {
|
|
Layout.preferredWidth: Style.smallIconSize + Style.smallMargins
|
|
font: Style.smallFont
|
|
text: neighborTableDelegate.neighbor.depth
|
|
horizontalAlignment: Text.AlignRight
|
|
}
|
|
Item {
|
|
Layout.preferredWidth: Style.smallIconSize
|
|
Layout.preferredHeight: Style.smallIconSize
|
|
|
|
Led {
|
|
anchors.fill: parent
|
|
anchors.margins: Style.smallIconSize / 4
|
|
state: neighborTableDelegate.neighbor.permitJoining ? "on" : "off"
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
ColumnLayout {
|
|
visible: infoSelectionTabs.currentIndex == 2
|
|
RowLayout {
|
|
Label {
|
|
id: toLabel
|
|
text: qsTr("To")
|
|
font: Style.smallFont
|
|
Layout.fillWidth: true
|
|
}
|
|
Label {
|
|
id: viaLabel
|
|
text: qsTr("Via")
|
|
font: Style.smallFont
|
|
Layout.fillWidth: true
|
|
}
|
|
ColorIcon {
|
|
size: Style.smallIconSize
|
|
name: "transfer-progress"
|
|
}
|
|
}
|
|
ThinDivider {
|
|
color: Style.foregroundColor
|
|
}
|
|
ListView {
|
|
id: routesListView
|
|
Layout.fillWidth: true
|
|
implicitHeight: Math.min(root.height / 4, count * Style.smallIconSize)
|
|
clip: true
|
|
model: d.selectedNode ? d.selectedNode.routes.length : 0
|
|
|
|
delegate: RowLayout {
|
|
id: routesTableDelegate
|
|
width: routesListView.width
|
|
property ZigbeeNodeRoute route: d.selectedNode.routes[index]
|
|
property ZigbeeNode destinationNode: root.network.nodes.getNodeByNetworkAddress(route.destinationAddress)
|
|
property Thing destinationNodeThing: {
|
|
for (var i = 0; i < engine.thingManager.things.count; i++) {
|
|
var thing = engine.thingManager.things.get(i)
|
|
var param = thing.paramByName("ieeeAddress")
|
|
if (param && param.value == destinationNode.ieeeAddress) {
|
|
return thing
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
property ZigbeeNode nextHopNode: root.network.nodes.getNodeByNetworkAddress(route.nextHopAddress)
|
|
property Thing nextHopNodeThing: {
|
|
if (!nextHopNode) {
|
|
return null
|
|
}
|
|
|
|
for (var i = 0; i < engine.thingManager.things.count; i++) {
|
|
var thing = engine.thingManager.things.get(i)
|
|
var param = thing.paramByName("ieeeAddress")
|
|
if (param && param.value === nextHopNode.ieeeAddress) {
|
|
return thing
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
Label {
|
|
Layout.preferredWidth: toLabel.width
|
|
elide: Text.ElideRight
|
|
font: Style.smallFont
|
|
text: routesTableDelegate.route.destinationAddress === 0
|
|
? Configuration.systemName
|
|
: routesTableDelegate.destinationNodeThing
|
|
? routesTableDelegate.destinationNodeThing.name
|
|
: routesTableDelegate.destinationNode
|
|
? routesTableDelegate.destinationNode.model
|
|
: "0x" + routesTableDelegate.route.destinationAddress.toString(16)
|
|
}
|
|
Label {
|
|
Layout.preferredWidth: viaLabel.width
|
|
elide: Text.ElideRight
|
|
font: Style.smallFont
|
|
text: routesTableDelegate.route.nextHopAddress === 0
|
|
? Configuration.systemName
|
|
: routesTableDelegate.nextHopNodeThing
|
|
? routesTableDelegate.nextHopNodeThing.name
|
|
: routesTableDelegate.nextHopNode
|
|
? routesTableDelegate.nextHopNode.model
|
|
: "0x" + routesTableDelegate.route.nextHopAddress.toString(16)
|
|
}
|
|
ColorIcon {
|
|
name: {
|
|
switch (routesTableDelegate.route.status) {
|
|
case ZigbeeNode.ZigbeeNodeRouteStatusActive:
|
|
return "tick"
|
|
case ZigbeeNode.ZigbeeNodeRouteStatusDiscoveryFailed:
|
|
return "dialog-error-symbolic"
|
|
case ZigbeeNode.ZigbeeNodeRouteStatusDiscoveryUnderway:
|
|
return "find"
|
|
case ZigbeeNode.ZigbeeNodeRouteStatusInactive:
|
|
return "dialog-warning-symbolic"
|
|
case ZigbeeNode.ZigbeeNodeRouteStatusValidationUnderway:
|
|
return "system-update"
|
|
}
|
|
}
|
|
size: Style.smallIconSize
|
|
color: routesTableDelegate.route.memoryConstrained ? Style.orange : Style.foregroundColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: zigbeeHelpDialog
|
|
|
|
NymeaDialog {
|
|
id: dialog
|
|
title: qsTr("ZigBee topology help")
|
|
|
|
Flickable {
|
|
implicitHeight: helpColumn.implicitHeight
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
contentHeight: helpColumn.implicitHeight
|
|
clip: true
|
|
|
|
ColumnLayout {
|
|
id: helpColumn
|
|
width: parent.width
|
|
|
|
ListSectionHeader {
|
|
text: qsTr("Links")
|
|
}
|
|
|
|
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
size: Style.iconSize
|
|
name: "zigbee/zigbee-coordinator"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Node relationship")
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "zigbee/zigbee-sibling"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Sibling")
|
|
}
|
|
}
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "zigbee/zigbee-parent"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Parent")
|
|
}
|
|
}
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "zigbee/zigbee-child"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Child")
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "zigbee/zigbee-previous-child"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Previous child")
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "/ui/images/arrow-down.svg"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Depth in network")
|
|
}
|
|
}
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "add"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Permit join")
|
|
}
|
|
}
|
|
|
|
ListSectionHeader {
|
|
text: qsTr("Routes")
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "transfer-progress"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Route status")
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "tick"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Route active")
|
|
}
|
|
}
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "dialog-warning-symbolic"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Route inactive")
|
|
}
|
|
}
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "dialog-error-symbolic"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Route failed")
|
|
}
|
|
}
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "find"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Discovery in progress")
|
|
}
|
|
}
|
|
RowLayout {
|
|
spacing: Style.margins
|
|
ColorIcon {
|
|
Layout.preferredHeight: Style.iconSize
|
|
Layout.preferredWidth: Style.iconSize
|
|
name: "system-update"
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Validation in progress")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|