diff --git a/libnymea-app/zigbee/zigbeemanager.cpp b/libnymea-app/zigbee/zigbeemanager.cpp index c50aab3a..3ab45bfa 100644 --- a/libnymea-app/zigbee/zigbeemanager.cpp +++ b/libnymea-app/zigbee/zigbeemanager.cpp @@ -30,7 +30,10 @@ #include "zigbeemanager.h" +#include + #include "engine.h" +#include "logging.h" #include "jsonrpc/jsonrpcclient.h" #include "zigbee/zigbeeadapter.h" #include "zigbee/zigbeeadapters.h" @@ -39,9 +42,6 @@ #include "zigbee/zigbeenode.h" #include "zigbee/zigbeenodes.h" -#include - -#include "logging.h" NYMEA_LOGGING_CATEGORY(dcZigbee, "Zigbee") ZigbeeManager::ZigbeeManager(QObject *parent) : diff --git a/libnymea-app/zigbee/zigbeemanager.h b/libnymea-app/zigbee/zigbeemanager.h index b54cee74..920165cd 100644 --- a/libnymea-app/zigbee/zigbeemanager.h +++ b/libnymea-app/zigbee/zigbeemanager.h @@ -105,6 +105,7 @@ private: ZigbeeAdapter *unpackAdapter(const QVariantMap &adapterMap); ZigbeeNetwork *unpackNetwork(const QVariantMap &networkMap); ZigbeeNode *unpackNode(const QVariantMap &nodeMap); + void fillNetworkData(ZigbeeNetwork *network, const QVariantMap &networkMap); void addOrUpdateNode(ZigbeeNetwork *network, const QVariantMap &nodeMap); }; diff --git a/libnymea-app/zigbee/zigbeenodesproxy.cpp b/libnymea-app/zigbee/zigbeenodesproxy.cpp index 7fae0f59..f145529b 100644 --- a/libnymea-app/zigbee/zigbeenodesproxy.cpp +++ b/libnymea-app/zigbee/zigbeenodesproxy.cpp @@ -56,13 +56,15 @@ void ZigbeeNodesProxy::setZigbeeNodes(ZigbeeNodes *zigbeeNodes) qWarning() << "Set nodes to proxy" << m_zigbeeNodes->rowCount(); connect(m_zigbeeNodes, &ZigbeeNodes::countChanged, this, [this](){ + sort(0, Qt::AscendingOrder); emit countChanged(); }); setSourceModel(m_zigbeeNodes); + + // Sort by network address so the coordinator will always be on the top setSortRole(ZigbeeNodes::RoleNetworkAddress); - sort(0, Qt::DescendingOrder); - invalidateFilter(); + sort(0, Qt::AscendingOrder); emit countChanged(); } diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index f11b47e0..9f683383 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -263,5 +263,7 @@ ui/images/setupwizard/wired-connection.svg ui/images/setupwizard/wireless-connection.svg ui/images/system-suspend.svg + ui/images/zigbee-enddevice.svg + ui/images/zigbee-router.svg diff --git a/nymea-app/ui/images/zigbee-enddevice.svg b/nymea-app/ui/images/zigbee-enddevice.svg new file mode 100644 index 00000000..8a9e545c --- /dev/null +++ b/nymea-app/ui/images/zigbee-enddevice.svg @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/zigbee-router.svg b/nymea-app/ui/images/zigbee-router.svg new file mode 100644 index 00000000..796c3bc9 --- /dev/null +++ b/nymea-app/ui/images/zigbee-router.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/system/ZigbeeAddNetworkPage.qml b/nymea-app/ui/system/ZigbeeAddNetworkPage.qml index 4ef8d894..5ab8a972 100644 --- a/nymea-app/ui/system/ZigbeeAddNetworkPage.qml +++ b/nymea-app/ui/system/ZigbeeAddNetworkPage.qml @@ -52,7 +52,6 @@ SettingsPageBase { } } - Connections { target: root.zigbeeManager onAddNetworkReply: { diff --git a/nymea-app/ui/system/ZigbeeNetworkPage.qml b/nymea-app/ui/system/ZigbeeNetworkPage.qml index 27893c4a..252a3a14 100644 --- a/nymea-app/ui/system/ZigbeeNetworkPage.qml +++ b/nymea-app/ui/system/ZigbeeNetworkPage.qml @@ -46,6 +46,12 @@ SettingsPageBase { backButtonVisible: true onBackPressed: pageStack.pop() + HeaderButton { + imageSource: "/ui/images/help.svg" + text: qsTr("Network settings") + onClicked: pageStack.push(zigbeeHelpPage) + } + HeaderButton { imageSource: "/ui/images/configure.svg" text: qsTr("Network settings") @@ -54,6 +60,7 @@ SettingsPageBase { } busy: d.pendingCommandId != -1 + QtObject { id: d property int pendingCommandId: -1 @@ -91,128 +98,141 @@ SettingsPageBase { text: qsTr("Network") } - RowLayout { - Layout.fillWidth: true + ColumnLayout { + spacing: app.margins Layout.leftMargin: app.margins Layout.rightMargin: app.margins - 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") - } - } - } - - 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 - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - text: qsTr("Channel") + ": " + network.channel - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - - Label { + RowLayout { Layout.fillWidth: true - text: qsTr("Permit new devices:") - } - Label { - text: network.permitJoiningEnabled ? qsTr("Open for %0 s").arg(network.permitJoiningRemaining) : qsTr("Closed") - } - ColorIcon { - Layout.preferredHeight: Style.iconSize - Layout.preferredWidth: Style.iconSize - name: network.permitJoiningEnabled ? "/ui/images/lock-open.svg" : "/ui/images/lock-closed.svg" - visible: !network.permitJoiningEnabled - } - 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() + Label { + Layout.fillWidth: true + text: qsTr("Network state:") } - onPaint: { - var ctx = canvas.getContext("2d"); - ctx.save(); - ctx.reset(); - var data = [1 - progress, progress]; - var myTotal = 0; + Label { + 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") + } + } + } - for(var e = 0; e < data.length; e++) { - myTotal += data[e]; + 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" + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + text: qsTr("Channel") + } + + Label { + text: network.channel + } + } + + RowLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + text: qsTr("Permit new devices:") + } + + Label { + text: network.permitJoiningEnabled ? qsTr("Open for %0 s").arg(network.permitJoiningRemaining) : qsTr("Closed") + } + + ColorIcon { + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: Style.iconSize + name: network.permitJoiningEnabled ? "/ui/images/lock-open.svg" : "/ui/images/lock-closed.svg" + visible: !network.permitJoiningEnabled + } + + 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() } - ctx.fillStyle = Style.accentColor - ctx.strokeStyle = Style.accentColor - ctx.lineWidth = 1; + onPaint: { + var ctx = canvas.getContext("2d"); + ctx.save(); + ctx.reset(); + var data = [1 - progress, progress]; + var myTotal = 0; - 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(); + for(var e = 0; e < data.length; e++) { + myTotal += data[e]; + } - ctx.restore(); + ctx.fillStyle = Style.accentColor + ctx.strokeStyle = Style.accentColor + 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(); + } } } + + Button { + Layout.fillWidth: true + text: network.permitJoiningEnabled ? qsTr("Extend open duration") : qsTr("Open for new devices") + enabled: network.networkState === ZigbeeNetwork.ZigbeeNetworkStateOnline + onClicked: zigbeeManager.setPermitJoin(network.networkUuid) + } + + } - Button { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - - text: network.permitJoiningEnabled ? qsTr("Extend open duration") : qsTr("Open for new devices") - enabled: network.networkState === ZigbeeNetwork.ZigbeeNetworkStateOnline - onClicked: zigbeeManager.setPermitJoin(network.networkUuid) - } - - SettingsPageSectionHeader { text: qsTr("Zigbee nodes") } @@ -225,25 +245,32 @@ SettingsPageBase { } delegate: BigTile { - - property ZigbeeNode node: root.network.nodes.get(index) - Layout.fillWidth: true interactive: false + property ZigbeeNode node: zigbeeNodesProxy.get(index) + contentItem: ColumnLayout { spacing: app.margins Loader { id: nodeTypeLoader Layout.fillWidth: true - sourceComponent: node.type === ZigbeeNode.ZigbeeNodeTypeCoordinator ? coordinatorComponent : deviceComponent + sourceComponent: node ? (node.type === ZigbeeNode.ZigbeeNodeTypeCoordinator ? zigbeeCoordinatorComponent : zigbeeDeviceComponent) : null } Component { - id: coordinatorComponent + id: zigbeeCoordinatorComponent ColumnLayout { RowLayout { + + ColorIcon { + id: coordinatorIcon + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: Style.iconSize + name: "/ui/images/zigbee.svg" + } + Led { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize @@ -265,18 +292,19 @@ SettingsPageBase { Label { Layout.fillWidth: true - text: qsTr("Coordinator") + text: network.backend + " " + qsTr("network coordinator") } } - Label { text: network.backend } Label { visible: node.version !== "" text: qsTr("Version") + ": " + network.firmwareVersion } + Label { text: qsTr("IEEE address") + ": " + node.ieeeAddress } + Label { text: qsTr("Network address") + ": 0x" + (node.networkAddress + Math.pow(16, 4)).toString(16).slice(-4).toUpperCase(); } @@ -284,15 +312,36 @@ SettingsPageBase { } Component { - id: deviceComponent + id: zigbeeDeviceComponent ColumnLayout { RowLayout { + id: nodeHeaderRowLayout + ColorIcon { + id: deviceTypeIcon + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: Style.iconSize + name: node ? (node.type === ZigbeeNode.ZigbeeNodeTypeRouter ? "/ui/images/zigbee-router.svg" : "/ui/images/zigbee-enddevice.svg") : "/ui/images/zigbee-enddevice.svg" + } + Led { id: reachableLed Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize - state: node.reachable ? "on" : "red" + state: { + if (!node) + return "off" + + if (node.state !== ZigbeeNode.ZigbeeNodeStateInitialized) { + return "orange" + } + + if (node.reachable) { + return "on" + } else { + return "red" + } + } } Connections { @@ -321,19 +370,49 @@ SettingsPageBase { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize running: visible - visible: node.state !== ZigbeeNode.ZigbeeNodeStateInitialized + visible: node ? node.state !== ZigbeeNode.ZigbeeNodeStateInitialized : false } Label { Layout.fillWidth: true - text: node.type === ZigbeeNode.ZigbeeNodeTypeRouter ? qsTr("Router") : qsTr("End device") + text: node ? node.model : "" + } + + Label { + text: signalStrengthIcon.signalStrength + "%" + } + + ColorIcon { + id: signalStrengthIcon + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: Style.iconSize + + property int signalStrength: node ? Math.round(node.lqi * 100.0 / 255.0) : 0 + + name: { + if (!node || !node.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" + } } Loader { - id: iconLoader + id: sleepyIconLoader Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize - active: !node.rxOnWhenIdle + active: node ? !node.rxOnWhenIdle : false + visible: active sourceComponent: sleepyDeviceComponent } @@ -345,44 +424,146 @@ SettingsPageBase { } } - Label { - text: node.manufacturer + " - " + node.model - } + RowLayout { + id: nodeDescriptionRow + ColumnLayout { + Layout.fillWidth: true - Label { - visible: node.version !== "" - text: qsTr("Version") + ": " + node.version - } + Label { + text: node ? node.manufacturer + (node.version !== "" ? " (" + node.version + ")" : "") : "" + } - Label { - text: qsTr("IEEE address") + ": " + node.ieeeAddress - } + Label { + text: qsTr("IEEE address") + ": " + (node ? node.ieeeAddress : "") + } - Label { - text: qsTr("Network address") + ": 0x" + (node.networkAddress + Math.pow(16, 4)).toString(16).slice(-4).toUpperCase(); - } + Label { + Layout.fillWidth: true + text: qsTr("Network address") + (node ? ": 0x" + (node.networkAddress + Math.pow(16, 4)).toString(16).slice(-4).toUpperCase() : "") + } + } - Label { - text: qsTr("Signal strength") + ": " + Math.round(node.lqi * 100.0 / 255.0) + "%" - } + ColumnLayout { + Item { + // Spacing item + Layout.fillHeight: true + } - Button { - id: removeNodeButton - text: qsTr("Remove node") - onClicked: d.removeNode(network.networkUuid, node.ieeeAddress) + ProgressButton { + size: Style.iconSize + imageSource: "/ui/images/delete.svg" + longpressEnabled: false + onClicked: { + var dialog = removeZigbeeNodeDialogComponent.createObject(app, {zigbeeNode: node}) + dialog.open() + } + } + } } } } } } + } + + Component { + id: removeZigbeeNodeDialogComponent + + MeaDialog { + 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: zigbeeHelpPage + + SettingsPageBase { + id: root + title: qsTr("ZigBee network help") + + header: NymeaHeader { + text: qsTr("ZigBee network help") + backButtonVisible: true + onBackPressed: pageStack.pop() + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 2 * app.margins + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins - // NymeaSwipeDelegate { - // Layout.fillWidth: true - // iconName: "../images/zigbee.svg" - // text: node.manufacturer + " - " + node.model + " - " + node.version - // subText: node.ieeeAddress + " " + node.networkAddress - // } + RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: Style.iconSize + name: "/ui/images/zigbee.svg" + } + Label { + text: qsTr("Zigbee network coordinator") + } + } + + + RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: Style.iconSize + name: "/ui/images/zigbee-router.svg" + } + + Label { + text: qsTr("Zigbee router") + } + } + + RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: Style.iconSize + name: "/ui/images/zigbee-enddevice.svg" + } + + Label { + text: qsTr("Zigbee end device") + } + } + + RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: Style.iconSize + name: "/ui/images/system-suspend.svg" + } + + Label { + text: qsTr("Sleepy device") + } + } + } + } } }