/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU General Public License as published by the Free Software * Foundation, GNU version 3. This project 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 * this project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ import QtQuick 2.8 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.3 import "qrc:/ui/components" import Nymea 1.0 import NymeaApp.Utils 1.0 SettingsPageBase { id: root property ZigbeeManager zigbeeManager: null property ZigbeeNetwork network: null property ZigbeeNode node: null readonly property ZigbeeNode coordinatorNode: root.network.nodes.getNodeByNetworkAddress(0) header: NymeaHeader { text: qsTr("ZigBee node info") backButtonVisible: true onBackPressed: pageStack.pop() HeaderButton { imageSource: "/ui/images/delete.svg" text: qsTr("Remove node") onClicked: { var popup = removeZigbeeNodeDialogComponent.createObject(app, {zigbeeNode: root.node}) popup.open() } } } busy: d.pendingCommandId != -1 QtObject { id: d property int pendingCommandId: -1 function removeNode(networkUuid, ieeeAddress) { d.pendingCommandId = root.zigbeeManager.removeNode(networkUuid, ieeeAddress) } onPendingCommandIdChanged: { print("pendingCommandId changed", pendingCommandId, wakeupDialog) if (pendingCommandId == -1 && wakeupDialog != null) { wakeupDialog.close(); wakeupDialog.destroy(); wakeupDialog = null } } property var wakeupDialog: null } Connections { target: root.zigbeeManager onCreateBindingReply: { print("**** create binding reply", error) if (commandId == d.pendingCommandId) { d.pendingCommandId = -1 var props = {}; switch (error) { case ZigbeeManager.ZigbeeErrorNoError: return; case ZigbeeManager.ZigbeeErrorNetworkError: props.text = qsTr("An error happened in the ZigBee network when creating the binding."); break; case ZigbeeManager.ZigbeeErrorTimeoutError: props.text = qsTr("The ZigBee device did not respond. Please try again."); break; default: props.error = error; } var comp = Qt.createComponent("/ui/components/ErrorDialog.qml") var popup = comp.createObject(app, props) popup.open(); } } onRemoveBindingReply: { if (commandId == d.pendingCommandId) { d.pendingCommandId = -1 var props = {}; switch (error) { case ZigbeeManager.ZigbeeErrorNoError: return; case ZigbeeManager.ZigbeeErrorNetworkError: props.text = qsTr("An error happened in the ZigBee network when removing the binding."); break; case ZigbeeManager.ZigbeeErrorTimeoutError: props.text = qsTr("The ZigBee device did not respond. Please try again."); break; default: props.error = error; } var comp = Qt.createComponent("/ui/components/ErrorDialog.qml") var popup = comp.createObject(app, props) popup.open(); } } onRemoveNodeReply: { if (commandId == d.pendingCommandId) { var props = {}; switch (error) { case ZigbeeManager.ZigbeeErrorNoError: pageStack.pop(); return; case ZigbeeManager.ZigbeeErrorNetworkError: props.text = qsTr("An error happened in the ZigBee network when creating the binding."); break; default: props.error = error; return; } var comp = Qt.createComponent("/ui/components/ErrorDialog.qml") var popup = comp.createObject(app, props) popup.open(); } } } ThingsProxy { id: nodeThings engine: _engine paramsFilter: {"ieeeAddress": root.node.ieeeAddress} } property int signalStrength: node ? Math.round(node.lqi * 100.0 / 255.0) : 0 NymeaItemDelegate { Layout.fillWidth: true text: root.node.model subText: qsTr("Model") prominentSubText: false progressive: false } NymeaItemDelegate { prominentSubText: false progressive: false Layout.fillWidth: true text: root.node.manufacturer subText: qsTr("Manufacturer") } NymeaItemDelegate { prominentSubText: false progressive: false Layout.fillWidth: true text: root.node.ieeeAddress subText: qsTr("IEEE address") font: Style.smallFont } NymeaItemDelegate { prominentSubText: false progressive: false Layout.fillWidth: true text: "0x" + root.node.networkAddress.toString(16) subText: qsTr("Network address") } NymeaItemDelegate { prominentSubText: false progressive: false Layout.fillWidth: true text: (root.node.lqi * 100 / 255).toFixed(0) + " %" subText: qsTr("Signal strength") } NymeaItemDelegate { prominentSubText: false progressive: false Layout.fillWidth: true text: root.node.version.length > 0 ? root.node.version : qsTr("Unknown") subText: qsTr("Version") } NymeaItemDelegate { Layout.fillWidth: true text: qsTr("Device endpoints") subText: qsTr("Show detailed information about the node") onClicked: pageStack.push(endpointsPageComponent) } SettingsPageSectionHeader { text: qsTr("Associated things") } Repeater { model: nodeThings delegate: NymeaItemDelegate { Layout.fillWidth: true property Thing thing: nodeThings.get(index) iconName: app.interfacesToIcon(thing.thingClass.interfaces) text: thing.name onClicked: pageStack.push("/ui/thingconfiguration/ConfigureThingPage.qml", {thing: thing}) } // delegate: RowLayout { // id: thingDelegate // Layout.leftMargin: Style.margins // Layout.rightMargin: Style.margins // property Thing thing: 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) // } // } } ColumnLayout { visible: engine.jsonRpcClient.ensureServerVersion("6.2") SettingsPageSectionHeader { text: qsTr("Bindings") } Repeater { model: root.node.bindings delegate: NymeaSwipeDelegate { id: bindingDelegate Layout.fillWidth: true property ZigbeeNodeBinding binding: root.node.bindings[index] property ZigbeeNode destinationNode: root.network.nodes.getNode(binding.destinationAddress) property ZigbeeNodeEndpoint endpoint: root.node.getEndpoint(binding.sourceEndpointId); property ZigbeeCluster inputCluster: endpoint ? endpoint.getInputCluster(binding.clusterId) : null property ZigbeeCluster outputCluster: endpoint ? endpoint.getOutputCluster(binding.clusterId) : null property ZigbeeCluster usedCluster: inputCluster ? inputCluster : outputCluster property Thing destinationThing: destinationThings.count > 0 ? destinationThings.get(0) : null ThingsProxy { id: destinationThings engine: _engine paramsFilter: {"ieeeAddress": bindingDelegate.binding.destinationAddress} } iconName: destinationNode && destinationNode == root.coordinatorNode ? "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) : destinationThing ? app.interfacesToIcon(destinationThing.thingClass.interfaces) : "/ui/images/zigbee.svg" canDelete: true progressive: false text: { if (binding.destinationAddress == "") { return qsTr("Group: 0x%1").arg(NymeaUtils.pad(binding.groupAddress.toString(16), 4)) } var ret = "" if (destinationNode) { if (destinationNode == root.coordinatorNode) { ret += Configuration.systemName } else { ret += destinationNode.model } } else { ret += binding.destinationAddress } if (destinationThings.count == 1) { ret += " (" + destinationThings.get(0).name + ")" } else if (destinationThings.count > 1) { ret += " (" + destinationThings.count + " things)" } return ret } subText: { var ret = usedCluster.clusterName(); if (binding.destinationAddress != "") { ret += " (" + binding.sourceEndpointId + " -> " + binding.destinationEndpointId + ")" } return ret; } onDeleteClicked: { if (!node.rxOnWhenIdle) { d.wakeupDialog = wakeupDialogComponent.createObject(root) d.wakeupDialog.open() } d.pendingCommandId = zigbeeManager.removeBinding(network.networkUuid, binding) } } } Button { Layout.fillWidth: true Layout.leftMargin: Style.margins Layout.rightMargin: Style.margins text: qsTr("Add binding") onClicked: { var dialog = addBindingComponent.createObject(root) dialog.open() } } } Component { id: removeZigbeeNodeDialogComponent NymeaDialog { id: removeZigbeeNodeDialog property ZigbeeNode zigbeeNode headerIcon: "/ui/images/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: endpointsPageComponent SettingsPageBase { title: qsTr("Node descriptor") header: NymeaHeader { text: qsTr("ZigBee node descriptor") backButtonVisible: true onBackPressed: pageStack.pop() HeaderButton { imageSource: "/ui/images/edit-copy.svg" text: qsTr("Copy") onClicked: { PlatformHelper.toClipBoard(descriptorText.text) ToolTip.show(qsTr("Copied to clipboard"), 1000); } } } TextArea { id: descriptorText readOnly: true Layout.fillWidth: true leftPadding: Style.margins rightPadding: Style.margins topPadding: Style.margins bottomPadding: Style.margins font.family: "UbuntuMono" font.pixelSize: Style.smallFont.pixelSize text: { var ret = "Manufacturer: " + root.node.manufacturer + "\n" ret += "Model: " + root.node.model + "\n" ret += "RxOnWhileIdle: " + root.node.rxOnWhenIdle + "\n" ret += "Basic cluster version: " + root.node.version + "\n" if (root.node.endpoints.length > 0) { ret += "Endpoints\n"; } for (var i = 0; i < root.node.endpoints.length; i++) { var endpoint = root.node.endpoints[i] var isLastEp = i == root.node.endpoints.length - 1; var hasInputClusters = endpoint.inputClusters.length > 0 var hasOutputClusters = endpoint.outputClusters.length > 0 ret += (isLastEp ? "└" : "├") + (hasInputClusters || hasOutputClusters ? "┬" : "─") + " " + endpoint.endpointId + "\n" ret += (isLastEp ? " " : "│") + (hasOutputClusters ? "├" : "└") + (hasInputClusters ? "┬" : "─") + " Input clusters\n" for (var j = 0; j < endpoint.inputClusters.length; j++) { var cluster = endpoint.inputClusters[j] var isLast = j == endpoint.inputClusters.length - 1 ret += (isLastEp ? " " : "│") + (hasOutputClusters ? "│" : " ") + (isLast ? "└" : "├") + "─ 0x" + NymeaUtils.pad(cluster.clusterId, 4, 16) + " " + cluster.clusterName() + " (" + (cluster.direction == ZigbeeCluster.ZigbeeClusterDirectionClient ? qsTr("Client") : qsTr("Server")) + ")\n" } if (hasOutputClusters) { ret += (isLastEp ? " " : "│") + "└" + (hasOutputClusters ? "┬" : "─") +" Output clusters\n" for (var j = 0; j < endpoint.outputClusters.length; j++) { var cluster = endpoint.outputClusters[j] var isLast = j == endpoint.outputClusters.length - 1 ret += (isLastEp ? " " : "│") + " " + (isLast ? "└" : "├") + "─ 0x" + NymeaUtils.pad(cluster.clusterId, 4, 16) + " " + cluster.clusterName() + " (" + (cluster.direction == ZigbeeCluster.ZigbeeClusterDirectionClient ? qsTr("Client") : qsTr("Server")) + ")\n" } } } return ret; } } } } Component { id: addBindingComponent NymeaDialog { title: qsTr("Add binding") Label { text: qsTr("Source endpoint") Layout.fillWidth: true } ComboBox { Layout.fillWidth: true id: sourceEndpointComboBox model: root.node.endpoints textRole: "endpointId" displayText: currentEndpoint.endpointId property ZigbeeNodeEndpoint currentEndpoint: root.node.endpoints[currentIndex] onCurrentEndpointChanged: print("source endpoint changed", currentEndpoint.endpointId) } Label { text: qsTr("Target") Layout.fillWidth: true } RowLayout { // Groups not properly implemented yet. hiding it for now visible: false RadioButton { id: nodeRadioButton text: qsTr("Node") checked: true Layout.fillWidth: true } RadioButton { id: groupRadioButton text: qsTr("Group") Layout.fillWidth: true } } ComboBox { id: destinationNodeComboBox Layout.fillWidth: true Layout.preferredHeight: Style.delegateHeight model: network.nodes visible: nodeRadioButton.checked property ZigbeeNode currentNode: network.nodes.get(currentIndex) property Thing currentNodeThing: currentNode && currentDestinationNodeThings.count > 0 ? currentDestinationNodeThings.get(0) : null ThingsProxy { id: currentDestinationNodeThings engine: _engine paramsFilter: destinationNodeComboBox.currentNode ? {"ieeeAddress": destinationNodeComboBox.currentNode.ieeeAddress} : {} } contentItem: RowLayout { id: destinationNodeContentItem width: parent.width - destinationNodeComboBox.indicator.width - Style.smallMargins height: Style.delegateHeight spacing: Style.smallMargins ColorIcon { Layout.leftMargin: Style.smallMargins size: Style.iconSize name: destinationNodeComboBox.currentNode == root.coordinatorNode ? "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) : destinationNodeComboBox.currentNodeThing ? app.interfacesToIcon(destinationNodeComboBox.currentNodeThing.thingClass.interfaces) : "/ui/images/zigbee.svg" color: Style.accentColor } ColumnLayout { Label { Layout.fillWidth: true text: destinationNodeComboBox.currentNode == root.coordinatorNode ? Configuration.systemName : destinationNodeComboBox.currentNode.model + " - " + destinationNodeComboBox.currentNode.manufacturer elide: Text.ElideRight } Label { Layout.fillWidth: true text: destinationNodeComboBox.currentNode == root.coordinatorNode ? qsTr("Coordinator") : currentDestinationNodeThings.count == 1 ? destinationNodeComboBox.currentNodeThing.name : currentDestinationNodeThings.count > 1 ? qsTr("%1 things").arg(currentDestinationNodeThings.count) : qsTr("Unrecognized device") font: Style.smallFont elide: Text.ElideRight } } } delegate: NymeaItemDelegate { id: destinationNodeDelegate property ZigbeeNode node: network.nodes.get(index) property Thing nodeThing: destinationNodeThings.count > 0 ? destinationNodeThings.get(0) : null iconName: node == coordinatorNode ? "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) : nodeThing ? app.interfacesToIcon(nodeThing.thingClass.interfaces) : "/ui/images/zigbee.svg" width: parent.width text: node == root.coordinatorNode ? Configuration.systemName : node.model + " - " + node.manufacturer subText: node == root.coordinatorNode ? qsTr("Coordinator") : destinationNodeThings.count == 1 ? nodeThing.name : destinationNodeThings.count > 1 ? qsTr("%1 things").arg(destinationNodeThings.count) : qsTr("Unrecognized device") progressive: false ThingsProxy { id: destinationNodeThings engine: _engine paramsFilter: {"ieeeAddress": destinationNodeDelegate.node.ieeeAddress} } } } Label { visible: nodeRadioButton.checked text: qsTr("Destination endpoint") Layout.fillWidth: true } ComboBox { id: destinationEndpointComboBox visible: nodeRadioButton.checked Layout.fillWidth: true model: destinationNodeComboBox.currentNode.endpoints textRole: "endpointId" displayText: currentEndpoint.endpointId property ZigbeeNodeEndpoint currentEndpoint: destinationNodeComboBox.currentNode.endpoints[currentIndex] } Label { text: qsTr("Group address") visible: groupRadioButton.checked } TextField { id: groupAddressTextField visible: groupRadioButton.checked Layout.fillWidth: true text: "0" } Label { text: qsTr("Cluster") Layout.fillWidth: true } ComboBox { id: clusterComboBox Layout.fillWidth: true delegate: ItemDelegate { width: parent.width text: modelData.clusterName() } model: { var ret = [] if (nodeRadioButton.checked) { print("updating clusters", sourceEndpointComboBox.currentEndpoint, destinationNodeComboBox.currentNode, destinationEndpointComboBox.currentEndpoint) if (!sourceEndpointComboBox.currentEndpoint || !destinationNodeComboBox.currentNode || !destinationEndpointComboBox.currentEndpoint) { return ret; } if (destinationNodeComboBox.currentNode == root.coordinatorNode) { for (var i = 0; i < sourceEndpointComboBox.currentEndpoint.outputClusters.length; i++) { var outputCluster = sourceEndpointComboBox.currentEndpoint.outputClusters[i] ret.push(outputCluster) } for (var i = 0; i < sourceEndpointComboBox.currentEndpoint.inputClusters.length; i++) { var inputCluster = sourceEndpointComboBox.currentEndpoint.inputClusters[i] ret.push(inputCluster) } } else { for (var i = 0; i < sourceEndpointComboBox.currentEndpoint.outputClusters.length; i++) { var outputCluster = sourceEndpointComboBox.currentEndpoint.outputClusters[i] print("source has cluster", outputCluster.clusterId) for (var j = 0; j < destinationEndpointComboBox.currentEndpoint.inputClusters.length; j++) { var inputCluster = destinationEndpointComboBox.currentEndpoint.inputClusters[j] print("destination has cluster", inputCluster.clusterId) if (inputCluster.clusterId === outputCluster.clusterId && inputCluster.direction !== outputCluster.direction) { ret.push(outputCluster); break; } } } for (var i = 0; i < sourceEndpointComboBox.currentEndpoint.inputClusters.length; i++) { var inputCluster = sourceEndpointComboBox.currentEndpoint.inputClusters[i] print("source has cluster", inputCluster.clusterId) for (var j = 0; j < destinationEndpointComboBox.currentEndpoint.outputClusters.length; j++) { var outputCluster = destinationEndpointComboBox.currentEndpoint.outputClusters[j] print("destination has cluster", outputCluster.clusterId) if (inputCluster.clusterId === outputCluster.clusterId && inputCluster.direction !== outputCluster.direction) { ret.push(inputCluster); break; } } } } } else { for (var i = 0; i < sourceEndpointComboBox.currentEndpoint.inputClusters.length; i++) { var inputCluster = sourceEndpointComboBox.currentEndpoint.inputClusters[i] ret.push(inputCluster); } } return ret } property ZigbeeCluster currentCluster: currentValue displayText: currentValue.clusterName() } onAccepted: { if (nodeRadioButton.checked) { d.pendingCommandId = root.zigbeeManager.createBinding( root.network.networkUuid, root.node.ieeeAddress, sourceEndpointComboBox.currentEndpoint.endpointId, clusterComboBox.currentCluster.clusterId, destinationNodeComboBox.currentNode.ieeeAddress, destinationEndpointComboBox.currentEndpoint.endpointId) } else { d.pendingCommandId = root.zigbeeManager.createGroupBinding( root.network.networkUuid, root.node.ieeeAddress, sourceEndpointComboBox.currentEndpoint.endpointId, clusterComboBox.currentCluster.clusterId, groupAddressTextField.text) } if (!root.node.rxOnWhenIdle) { d.wakeupDialog = wakeupDialogComponent.createObject(root) d.wakeupDialog.open() } } } } Component { id: wakeupDialogComponent NymeaDialog { id: wakeupDialog title: qsTr("Wake up %1").arg(root.node.model) text: qsTr("The selected node is a sleepy device. Please wake up the device by pressing a button.") } } }