// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-app. * * nymea-app is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-app. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ import QtQuick import QtQuick.Controls import QtQuick.Controls.Material import QtQuick.Layouts import "qrc:/ui/components" import Nymea SettingsPageBase { id: root property ZigbeeManager zigbeeManager: null property ZigbeeNetwork network: null signal exit() header: NymeaHeader { text: qsTr("ZigBee network") backButtonVisible: true onBackPressed: pageStack.pop() HeaderButton { imageSource: "qrc:/icons/help.svg" text: qsTr("Help") onClicked: { var popup = zigbeeHelpDialog.createObject(app) popup.open() } } HeaderButton { imageSource: "qrc:/icons/configure.svg" text: qsTr("Network settings") onClicked: { var page = pageStack.push(Qt.resolvedUrl("ZigbeeNetworkSettingsPage.qml"), { zigbeeManager: zigbeeManager, network: network }) page.exit.connect(function() { root.exit() }) } } } busy: d.pendingCommandId != -1 QtObject { id: d property int pendingCommandId: -1 function removeNode(networkUuid, ieeeAddress) { d.pendingCommandId = root.zigbeeManager.removeNode(networkUuid, ieeeAddress) } } Connections { target: root.zigbeeManager onRemoveNodeReply: { if (commandId == d.pendingCommandId) { d.pendingCommandId = -1 var props = {}; switch (error) { case ZigbeeManager.ZigbeeErrorNoError: return; case ZigbeeManager.ZigbeeErrorAdapterNotAvailable: props.text = qsTr("The selected adapter is not available or the selected serial port configration is incorrect."); break; case ZigbeeManager.ZigbeeErrorAdapterAlreadyInUse: props.text = qsTr("The selected adapter is already in use."); break; default: props.error = error; } var comp = Qt.createComponent("/ui/components/ErrorDialog.qml") var popup = comp.createObject(app, props) popup.open(); } } } SettingsPageSectionHeader { text: qsTr("Network state") } ColumnLayout { spacing: Style.margins Layout.leftMargin: Style.margins Layout.rightMargin: Style.margins GridLayout { Layout.fillWidth: true columns: 2 rowSpacing: Style.margins columnSpacing: Style.margins RowLayout { Layout.preferredWidth: (parent.width - parent.columnSpacing) / 2 Led { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize state: { switch (network.networkState) { case ZigbeeNetwork.ZigbeeNetworkStateOnline: return "on" case ZigbeeNetwork.ZigbeeNetworkStateOffline: return "off" case ZigbeeNetwork.ZigbeeNetworkStateStarting: return "orange" case ZigbeeNetwork.ZigbeeNetworkStateUpdating: return "orange" case ZigbeeNetwork.ZigbeeNetworkStateError: return "red" } } } Label { Layout.fillWidth: true text: { switch (network.networkState) { case ZigbeeNetwork.ZigbeeNetworkStateOnline: return qsTr("Online") case ZigbeeNetwork.ZigbeeNetworkStateOffline: return qsTr("Offline") case ZigbeeNetwork.ZigbeeNetworkStateStarting: return qsTr("Starting") case ZigbeeNetwork.ZigbeeNetworkStateUpdating: return qsTr("Updating") case ZigbeeNetwork.ZigbeeNetworkStateError: return qsTr("Error") } } } } RowLayout { Layout.preferredWidth: (parent.width - parent.columnSpacing) / 2 Canvas { id: canvas Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize rotation: -90 visible: network.permitJoiningEnabled property real progress: network.permitJoiningRemaining / network.permitJoiningDuration onProgressChanged: { canvas.requestPaint() } onPaint: { var ctx = canvas.getContext("2d"); ctx.save(); ctx.reset(); var data = [1 - progress, progress]; var myTotal = 0; for(var e = 0; e < data.length; e++) { myTotal += data[e]; } ctx.fillStyle = Style.foregroundColor ctx.strokeStyle = Style.foregroundColor ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(canvas.width/2,canvas.height/2); ctx.arc(canvas.width/2,canvas.height/2,canvas.height/2,0,(Math.PI*2*((1-progress)/myTotal)),false); ctx.lineTo(canvas.width/2,canvas.height/2); ctx.fill(); ctx.closePath(); ctx.beginPath(); ctx.arc(canvas.width/2,canvas.height/2,canvas.height/2 - 1,0,Math.PI*2,false); ctx.closePath(); ctx.stroke(); ctx.restore(); } } ColorIcon { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize name: network.permitJoiningEnabled ? "qrc:/icons/lock-open.svg" : "qrc:/icons/lock-closed.svg" visible: !network.permitJoiningEnabled } Label { Layout.fillWidth: true text: network.permitJoiningEnabled ? qsTr("Open for %0 s").arg(network.permitJoiningRemaining) : qsTr("Closed") } } Button { Layout.fillWidth: true Layout.columnSpan: 2 text: qsTr("Open for new devices") enabled: network.networkState === ZigbeeNetwork.ZigbeeNetworkStateOnline visible: !network.permitJoiningEnabled onClicked: zigbeeManager.setPermitJoin(network.networkUuid) } Button { Layout.fillWidth: true text: qsTr("Extend") enabled: network.networkState === ZigbeeNetwork.ZigbeeNetworkStateOnline visible: network.permitJoiningEnabled onClicked: zigbeeManager.setPermitJoin(network.networkUuid, 254) } Button { Layout.fillWidth: true enabled: network.networkState == ZigbeeNetwork.ZigbeeNetworkStateOnline visible: network.permitJoiningEnabled text: qsTr("Close") onClicked: { zigbeeManager.setPermitJoin(network.networkUuid, 0) } } } } SettingsPageSectionHeader { text: offlineNodes.count == 0 ? qsTr("%n device(s)", "", Math.max(0, root.network.nodes.count - 1)) // -1 for coordinator node : qsTr("%n device(s) (%1 disconnected)", "", Math.max(root.network.nodes.count - 1)).arg(offlineNodes.count) ZigbeeNodesProxy { id: offlineNodes zigbeeNodes: root.network.nodes showCoordinator: false showOnline: false } } Label { Layout.fillWidth: true Layout.margins: Style.margins horizontalAlignment: Text.AlignHCenter text: qsTr("There are no ZigBee devices connected yet. Open the network for new devices to join and start the pairing procedure from the ZigBee device. Please refer to the devices manual for more information on how to start the pairing.") wrapMode: Text.WordWrap visible: nodesModel.count === 0 } Repeater { model: ZigbeeNodesProxy { id: nodesModel zigbeeNodes: root.network.nodes showCoordinator: false newOnTop: true } delegate: NymeaSwipeDelegate { id: nodeDelegate readonly property ZigbeeNode node: nodesModel.get(index) ThingsProxy { id: nodeThings engine: _engine paramsFilter: {"ieeeAddress": nodeDelegate.node.ieeeAddress} } readonly property Thing nodeThing: nodeThings.count >= 1 ? nodeThings.get(0) : null property int signalStrength: node ? Math.round(node.lqi * 100.0 / 255.0) : 0 Layout.fillWidth: true text: node.model + " - " + node.manufacturer// nodeThing ? nodeThing.name : node.model subText: node.state == ZigbeeNode.ZigbeeNodeStateInitializing ? qsTr("Initializing...") : nodeThings.count == 1 ? nodeThing.name : nodeThings.count > 1 ? qsTr("%1 things").arg(nodeThings.count) : qsTr("Unrecognized device") iconName: nodeThing ? app.interfacesToIcon(nodeThing.thingClass.interfaces) : "qrc:/icons/zigbee.svg" iconColor: busy ? Style.tileOverlayColor : nodeThing != null ? Style.accentColor : Style.iconColor progressive: false busy: node.state !== ZigbeeNode.ZigbeeNodeStateInitialized canDelete: true onDeleteClicked: { var dialog = removeZigbeeNodeDialogComponent.createObject(app, {zigbeeNode: node}) dialog.open() } secondaryIconName: node && !node.rxOnWhenIdle ? "qrc:/icons/system-suspend.svg" : "" tertiaryIconName: { if (!node || !node.reachable) return "qrc:/icons/connections/nm-signal-00.svg" if (signalStrength <= 25) return "qrc:/icons/connections/nm-signal-25.svg" if (signalStrength <= 50) return "qrc:/icons/connections/nm-signal-50.svg" if (signalStrength <= 75) return "qrc:/icons/connections/nm-signal-75.svg" if (signalStrength <= 100) return "qrc:/icons/connections/nm-signal-100.svg" } tertiaryIconColor: node.reachable ? Style.iconColor : Style.red Connections { target: node onLastSeenChanged: communicationIndicatorLedTimer.start() } Timer { id: communicationIndicatorLedTimer interval: 200 } additionalItem: ColorIcon { size: Style.smallIconSize anchors.verticalCenter: parent.verticalCenter name: node.type === ZigbeeNode.ZigbeeNodeTypeCoordinator ? "qrc:/icons/zigbee/zigbee-coordinator.svg" : node.type === ZigbeeNode.ZigbeeNodeTypeRouter ? "qrc:/icons/zigbee/zigbee-router.svg" : "qrc:/icons/zigbee/zigbee-enddevice.svg" color: communicationIndicatorLedTimer.running ? Style.accentColor : Style.iconColor } onClicked: { pageStack.push("ZigbeeNodePage.qml", {zigbeeManager: zigbeeManager, network: network, node: node}) // var popup = nodeInfoComponent.createObject(app, {node: node, nodeThings: nodeThings}) // popup.open() } } } Component { id: nodeInfoComponent NymeaDialog { id: nodeInfoDialog property ZigbeeNode node: null property ThingsProxy nodeThings: null readonly property Thing nodeThing: nodeThings.count > 0 ? nodeThings.get(0) : null header: Item { implicitHeight: headerRow.height implicitWidth: parent.width RowLayout { id: headerRow anchors { left: parent.left; right: parent.right; top: parent.top; margins: Style.margins } spacing: Style.margins ColorIcon { id: headerColorIcon Layout.preferredHeight: Style.bigIconSize Layout.preferredWidth: height color: Style.accentColor name: "qrc:/icons/zigbee.svg" } ColumnLayout { Layout.margins: Style.margins Label { Layout.fillWidth: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere text: nodeInfoDialog.node.model } Label { Layout.fillWidth: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere text: nodeInfoDialog.node.manufacturer } } } } standardButtons: Dialog.NoButton GridLayout { columns: 2 Label { text: qsTr("IEEE address:") font: Style.smallFont } Label { Layout.fillWidth: true text: nodeInfoDialog.node.ieeeAddress font: Style.smallFont horizontalAlignment: Text.AlignRight } Label { text: qsTr("Network address:") font: Style.smallFont } Label { Layout.fillWidth: true text: "0x" + nodeInfoDialog.node.networkAddress.toString(16) font: Style.smallFont horizontalAlignment: Text.AlignRight } Label { text: qsTr("Signal strength:") font: Style.smallFont } Label { Layout.fillWidth: true text: (nodeInfoDialog.node.lqi * 100 / 255).toFixed(0) + " %" font: Style.smallFont horizontalAlignment: Text.AlignRight } Label { text: qsTr("Version:") font: Style.smallFont } Label { Layout.fillWidth: true text: nodeInfoDialog.node.version.length > 0 ? nodeInfoDialog.node.version : qsTr("Unknown") font: Style.smallFont horizontalAlignment: Text.AlignRight } } SettingsPageSectionHeader { text: qsTr("Associated things") Layout.leftMargin: 0 Layout.rightMargin: 0 } Repeater { model: nodeInfoDialog.nodeThings delegate: RowLayout { id: thingDelegate property Thing thing: nodeInfoDialog.nodeThings.get(index) Layout.fillWidth: true ColorIcon { size: Style.iconSize source: app.interfacesToIcon(thing.thingClass.interfaces) color: Style.accentColor } TextField { text: thingDelegate.thing.name Layout.fillWidth: true onEditingFinished: engine.thingManager.editThing(thingDelegate.thing.id, text) } } } RowLayout { Layout.fillWidth: true Button { // size: Style.iconSize visible: node && node.type !== ZigbeeNode.ZigbeeNodeTypeCoordinator // imageSource: "qrc:/icons/delete.svg" text: qsTr("Remove") Layout.alignment: Qt.AlignLeft onClicked: { var dialog = removeZigbeeNodeDialogComponent.createObject(app, {zigbeeNode: node}) dialog.open() nodeInfoDialog.close() } } Item { Layout.fillWidth: true } Button { text: qsTr("OK") onClicked: nodeInfoDialog.close() Layout.alignment: Qt.AlignRight } } } } Component { id: removeZigbeeNodeDialogComponent NymeaDialog { id: removeZigbeeNodeDialog property ZigbeeNode zigbeeNode headerIcon: "qrc:/icons/zigbee.svg" title: qsTr("Remove ZigBee node") + " " + (zigbeeNode ? zigbeeNode.model : "") text: qsTr("Are you sure you want to remove this node from the network?") standardButtons: Dialog.Ok | Dialog.Cancel Label { text: qsTr("Please note that if this node has been assigned to a thing, it will also be removed from the system.") Layout.fillWidth: true wrapMode: Text.WordWrap } onAccepted: { d.removeNode(zigbeeNode.networkUuid, zigbeeNode.ieeeAddress) } } } Component { id: zigbeeHelpDialog NymeaDialog { id: dialog title: qsTr("ZigBee network help") RowLayout { spacing: Style.margins ColorIcon { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize name: "qrc:/icons/zigbee/zigbee-router.svg" } Label { text: qsTr("ZigBee router") } } RowLayout { spacing: Style.margins ColorIcon { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize name: "qrc:/icons/zigbee/zigbee-enddevice.svg" } Label { text: qsTr("ZigBee end device") } } RowLayout { spacing: Style.margins ColorIcon { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize name: "qrc:/icons/system-suspend.svg" } Label { text: qsTr("Sleepy device") } } } } }