569 lines
20 KiB
QML
569 lines
20 KiB
QML
// 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 <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|