diff --git a/libnymea-app/connection/nymeaconnection.cpp b/libnymea-app/connection/nymeaconnection.cpp index 16ae76e5..2c8a17b1 100644 --- a/libnymea-app/connection/nymeaconnection.cpp +++ b/libnymea-app/connection/nymeaconnection.cpp @@ -85,7 +85,7 @@ NymeaConnection::NymeaConnection(QObject *parent) : QObject(parent) updateActiveBearers(); - m_reconnectTimer.setInterval(100); + m_reconnectTimer.setInterval(500); m_reconnectTimer.setSingleShot(true); connect(&m_reconnectTimer, &QTimer::timeout, this, [this](){ if (m_currentHost && !m_currentTransport) { diff --git a/libnymea-app/connection/tunnelproxytransport.cpp b/libnymea-app/connection/tunnelproxytransport.cpp index 4bda46fe..c1748406 100644 --- a/libnymea-app/connection/tunnelproxytransport.cpp +++ b/libnymea-app/connection/tunnelproxytransport.cpp @@ -49,9 +49,9 @@ TunnelProxyTransport::TunnelProxyTransport(QObject *parent) : QObject::connect(m_remoteConnection, &TunnelProxyRemoteConnection::dataReady, this, &TunnelProxyTransport::dataReady); QObject::connect(m_remoteConnection, &TunnelProxyRemoteConnection::errorOccurred, this, &TunnelProxyTransport::onRemoteConnectionErrorOccurred); QObject::connect(m_remoteConnection, &TunnelProxyRemoteConnection::sslErrors, this, [=](const QList &errors){ - qWarning() << "Remote tunnel proxy server SSL errors occurred:"; + qCWarning(dcTunnelProxyRemoteConnectionDummy) << "Remote tunnel proxy server SSL errors occurred:"; foreach (const QSslError &sslError, errors) { - qWarning() << " --> " << sslError.errorString(); + qCWarning(dcTunnelProxyRemoteConnectionDummy) << " --> " << sslError.errorString(); } }); @@ -67,8 +67,6 @@ bool TunnelProxyTransport::connect(const QUrl &url) serverUrl.setPort(url.port()); QUuid serverUuid = QUrlQuery(url).queryItemValue("uuid"); - qCritical() << "Calling connect on" << serverUrl << serverUuid; - return m_remoteConnection->connectServer(serverUrl, serverUuid); } @@ -137,7 +135,7 @@ void TunnelProxyTransport::onRemoteConnectionStateChanged(remoteproxyclient::Tun void TunnelProxyTransport::onRemoteConnectionErrorOccurred(QAbstractSocket::SocketError error) { - qWarning() << "Tunnel proxy socket error occurred" << error; + qCWarning(dcTunnelProxyRemoteConnectionDummy) << "Tunnel proxy socket error occurred" << error; } NymeaTransportInterface *TunnelProxyTransportFactory::createTransport(QObject *parent) const diff --git a/libnymea-app/libnymea-app-core.h b/libnymea-app/libnymea-app-core.h index fa7b0265..7a9bd264 100644 --- a/libnymea-app/libnymea-app-core.h +++ b/libnymea-app/libnymea-app-core.h @@ -329,6 +329,7 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "ZigbeeNetwork", "Get it from the ZigbeeManager"); qmlRegisterUncreatableType(uri, 1, 0, "ZigbeeNetworks", "Get it from the ZigbeeManager"); qmlRegisterUncreatableType(uri, 1, 0, "ZigbeeNode", "Get it from the ZigbeeNodes"); + qmlRegisterUncreatableType(uri, 1, 0, "ZigbeeNodeNeighbor", "Get it from the ZigbeeNode"); qmlRegisterUncreatableType(uri, 1, 0, "ZigbeeNodes", "Get it from the ZigbeeNetwork"); qmlRegisterType(uri, 1, 0, "ZigbeeNodesProxy"); diff --git a/libnymea-app/zigbee/zigbeemanager.cpp b/libnymea-app/zigbee/zigbeemanager.cpp index ac8922c4..504098e7 100644 --- a/libnymea-app/zigbee/zigbeemanager.cpp +++ b/libnymea-app/zigbee/zigbeemanager.cpp @@ -231,7 +231,7 @@ void ZigbeeManager::factoryResetNetworkResponse(int commandId, const QVariantMap void ZigbeeManager::getNodesResponse(int commandId, const QVariantMap ¶ms) { - qCDebug(dcZigbee()) << "Zigbee get nodes response" << commandId << params; + qCDebug(dcZigbee()) << "Zigbee get nodes response" << commandId << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); foreach (const QVariant &nodeVariant, params.value("zigbeeNodes").toList()) { QVariantMap nodeMap = nodeVariant.toMap(); @@ -254,7 +254,7 @@ void ZigbeeManager::removeNodeResponse(int commandId, const QVariantMap ¶ms) void ZigbeeManager::notificationReceived(const QVariantMap ¬ification) { - qCDebug(dcZigbee()) << "Zigbee notification" << qUtf8Printable(QJsonDocument::fromVariant(notification).toJson()); +// qCDebug(dcZigbee()) << "Zigbee notification" << qUtf8Printable(QJsonDocument::fromVariant(notification).toJson()); QString notificationString = notification.value("notification").toString(); if (notificationString == "Zigbee.AdapterAdded") { QVariantMap adapterMap = notification.value("params").toMap().value("adapter").toMap(); @@ -360,7 +360,7 @@ ZigbeeNode *ZigbeeManager::unpackNode(const QVariantMap &nodeMap) QUuid networkUuid = nodeMap.value("networkUuid").toUuid(); QString ieeeAddress = nodeMap.value("ieeeAddress").toString(); ZigbeeNode *node = new ZigbeeNode(networkUuid, ieeeAddress, this); - node->updateNodeProperties(nodeMap); + updateNodeProperties(node, nodeMap); return node; } @@ -386,9 +386,35 @@ void ZigbeeManager::addOrUpdateNode(ZigbeeNetwork *network, const QVariantMap &n QString ieeeAddress = nodeMap.value("ieeeAddress").toString(); ZigbeeNode *node = network->nodes()->getNode(ieeeAddress); if (node) { - node->updateNodeProperties(nodeMap); + updateNodeProperties(node, nodeMap); } else { network->nodes()->addNode(unpackNode(nodeMap)); } } +void ZigbeeManager::updateNodeProperties(ZigbeeNode *node, const QVariantMap &nodeMap) +{ + node->setNetworkAddress(nodeMap.value("networkAddress").toUInt()); + node->setType(ZigbeeNode::stringToNodeType(nodeMap.value("type").toString())); + node->setState(ZigbeeNode::stringToNodeState(nodeMap.value("state").toString())); + node->setManufacturer(nodeMap.value("manufacturer").toString()); + node->setModel(nodeMap.value("model").toString()); + node->setVersion(nodeMap.value("version").toString()); + node->setRxOnWhenIdle(nodeMap.value("receiverOnWhileIdle").toBool()); + node->setReachable(nodeMap.value("reachable").toBool()); + node->setLqi(nodeMap.value("lqi").toUInt()); + node->setLastSeen(QDateTime::fromMSecsSinceEpoch(nodeMap.value("lastSeen").toUInt() * 1000)); + QList neighbors; + foreach (const QVariant &neighbor, nodeMap.value("neighborTableRecords").toList()) { + QVariantMap neighborMap = neighbor.toMap(); + quint16 networkAddress = neighborMap.value("networkAddress").toUInt(); +// qWarning() << "*********** adding neighbor" << networkAddress; + QMetaEnum relationshipEnum = QMetaEnum::fromType(); + ZigbeeNode::ZigbeeNodeRelationship relationship = static_cast(relationshipEnum.keyToValue(neighborMap.value("relationship").toByteArray().data())); + quint8 lqi = neighborMap.value("lqi").toUInt(); + quint8 depth = neighborMap.value("depth").toUInt(); + node->addOrUpdateNeighbor(networkAddress, relationship, lqi, depth); + neighbors.append(networkAddress); + } + node->commitNeighbors(neighbors); +} diff --git a/libnymea-app/zigbee/zigbeemanager.h b/libnymea-app/zigbee/zigbeemanager.h index 93658c85..96c2306e 100644 --- a/libnymea-app/zigbee/zigbeemanager.h +++ b/libnymea-app/zigbee/zigbeemanager.h @@ -133,6 +133,7 @@ private: void fillNetworkData(ZigbeeNetwork *network, const QVariantMap &networkMap); void addOrUpdateNode(ZigbeeNetwork *network, const QVariantMap &nodeMap); + void updateNodeProperties(ZigbeeNode *node, const QVariantMap &nodeMap); }; #endif // ZIGBEEMANAGER_H diff --git a/libnymea-app/zigbee/zigbeenode.cpp b/libnymea-app/zigbee/zigbeenode.cpp index 6d322355..65cde069 100644 --- a/libnymea-app/zigbee/zigbeenode.cpp +++ b/libnymea-app/zigbee/zigbeenode.cpp @@ -189,6 +189,51 @@ void ZigbeeNode::setLastSeen(const QDateTime &lastSeen) emit lastSeenChanged(m_lastSeen); } +QList ZigbeeNode::neighbors() const +{ + return m_neighbors; +} + +void ZigbeeNode::addOrUpdateNeighbor(quint16 networkAddress, ZigbeeNodeRelationship relationship, quint8 lqi, quint8 depth) +{ + foreach (ZigbeeNodeNeighbor *neighbor, m_neighbors) { + if (neighbor->networkAddress() == networkAddress) { + if (neighbor->relationship() != relationship) { + neighbor->setRelationship(relationship); + m_neighborsDirty = true; + } + if (neighbor->lqi() != lqi) { + neighbor->setLqi(lqi); + m_neighborsDirty = true; + } + return; + } + } + ZigbeeNodeNeighbor *neighbor = new ZigbeeNodeNeighbor(networkAddress, this); + neighbor->setRelationship(relationship); + neighbor->setLqi(lqi); + neighbor->setDepth(depth); + m_neighbors.append(neighbor); +} + +void ZigbeeNode::commitNeighbors(QList toBeKept) +{ + QMutableListIterator iter(m_neighbors); + + while (iter.hasNext()) { + ZigbeeNodeNeighbor *neighbor = iter.next(); + if (!toBeKept.contains(neighbor->networkAddress())) { + iter.remove(); + neighbor->deleteLater(); + m_neighborsDirty = true; + } + } + if (m_neighborsDirty) { + emit neighborsChanged(); + m_neighborsDirty = false; + } +} + ZigbeeNode::ZigbeeNodeState ZigbeeNode::stringToNodeState(const QString &nodeState) { if (nodeState == "ZigbeeNodeStateUninitialized") { @@ -213,16 +258,53 @@ ZigbeeNode::ZigbeeNodeType ZigbeeNode::stringToNodeType(const QString &nodeType) } } -void ZigbeeNode::updateNodeProperties(const QVariantMap &nodeMap) +ZigbeeNodeNeighbor::ZigbeeNodeNeighbor(quint16 networkAddress, QObject *parent): + QObject(parent), + m_networkAddress(networkAddress) { - setNetworkAddress(nodeMap.value("networkAddress").toUInt()); - setType(ZigbeeNode::stringToNodeType(nodeMap.value("type").toString())); - setState(ZigbeeNode::stringToNodeState(nodeMap.value("state").toString())); - setManufacturer(nodeMap.value("manufacturer").toString()); - setModel(nodeMap.value("model").toString()); - setVersion(nodeMap.value("version").toString()); - setRxOnWhenIdle(nodeMap.value("receiverOnWhileIdle").toBool()); - setReachable(nodeMap.value("reachable").toBool()); - setLqi(nodeMap.value("lqi").toUInt()); - setLastSeen(QDateTime::fromMSecsSinceEpoch(nodeMap.value("lastSeen").toUInt() * 1000)); + +} + +quint16 ZigbeeNodeNeighbor::networkAddress() const +{ + return m_networkAddress; +} + +ZigbeeNode::ZigbeeNodeRelationship ZigbeeNodeNeighbor::relationship() const +{ + return m_relationship; +} + +void ZigbeeNodeNeighbor::setRelationship(ZigbeeNode::ZigbeeNodeRelationship relationship) +{ + if (m_relationship != relationship) { + m_relationship = relationship; + emit relationshipChanged(); + } +} + +quint8 ZigbeeNodeNeighbor::lqi() const +{ + return m_lqi; +} + +void ZigbeeNodeNeighbor::setLqi(quint8 lqi) +{ + if (m_lqi != lqi) { + m_lqi = lqi; + emit lqiChanged(); + } +} + +quint8 ZigbeeNodeNeighbor::depth() const +{ + return m_depth; +} + +void ZigbeeNodeNeighbor::setDepth(quint8 depth) +{ + if (m_depth != depth) { + m_depth = depth; + emit depthChanged(); + } } diff --git a/libnymea-app/zigbee/zigbeenode.h b/libnymea-app/zigbee/zigbeenode.h index 03722bb1..9f1fe83e 100644 --- a/libnymea-app/zigbee/zigbeenode.h +++ b/libnymea-app/zigbee/zigbeenode.h @@ -36,6 +36,8 @@ #include #include +class ZigbeeNodeNeighbor; + class ZigbeeNode : public QObject { Q_OBJECT @@ -51,6 +53,7 @@ class ZigbeeNode : public QObject Q_PROPERTY(bool reachable READ reachable WRITE setReachable NOTIFY reachableChanged) Q_PROPERTY(uint lqi READ lqi WRITE setLqi NOTIFY lqiChanged) Q_PROPERTY(QDateTime lastSeen READ lastSeen WRITE setLastSeen NOTIFY lastSeenChanged) + Q_PROPERTY(QList neighbors READ neighbors NOTIFY neighborsChanged) public: enum ZigbeeNodeType { @@ -68,6 +71,15 @@ public: }; Q_ENUM(ZigbeeNodeState) + enum ZigbeeNodeRelationship { + ZigbeeNodeRelationshipParent, + ZigbeeNodeRelationshipChild, + ZigbeeNodeRelationshipSibling, + ZigbeeNodeRelationshipNone, + ZigbeeNodeRelationshipPreviousChild + }; + Q_ENUM(ZigbeeNodeRelationship) + explicit ZigbeeNode(const QUuid &networkUuid, const QString &ieeeAddress, QObject *parent = nullptr); QUuid networkUuid() const; @@ -103,11 +115,13 @@ public: QDateTime lastSeen() const; void setLastSeen(const QDateTime &lastSeen); + QList neighbors() const; + void addOrUpdateNeighbor(quint16 networkAddress, ZigbeeNodeRelationship relationship, quint8 lqi, quint8 depth); + void commitNeighbors(QList toBeKept); + static ZigbeeNodeState stringToNodeState(const QString &nodeState); static ZigbeeNodeType stringToNodeType(const QString &nodeType); - void updateNodeProperties(const QVariantMap &nodeMap); - signals: void networkAddressChanged(quint16 networkAddress); void typeChanged(ZigbeeNodeType type); @@ -119,6 +133,7 @@ signals: void reachableChanged(bool reachable); void lqiChanged(uint lqi); void lastSeenChanged(const QDateTime &lastSeen); + void neighborsChanged(); private: QUuid m_networkUuid; @@ -133,6 +148,42 @@ private: bool m_reachable = false; uint m_lqi = 0; QDateTime m_lastSeen; + QList m_neighbors; + bool m_neighborsDirty = false; +}; + +class ZigbeeNodeNeighbor: public QObject +{ + Q_OBJECT + Q_PROPERTY(quint16 networkAddress READ networkAddress CONSTANT) + Q_PROPERTY(ZigbeeNode::ZigbeeNodeRelationship relationship READ relationship NOTIFY relationshipChanged) + Q_PROPERTY(quint8 lqi READ lqi NOTIFY lqiChanged) + Q_PROPERTY(quint8 depth READ depth NOTIFY depthChanged) + +public: + ZigbeeNodeNeighbor(quint16 networkAddress, QObject *parent); + + quint16 networkAddress() const; + + ZigbeeNode::ZigbeeNodeRelationship relationship() const; + void setRelationship(ZigbeeNode::ZigbeeNodeRelationship relationship); + + quint8 lqi() const; + void setLqi(quint8 lqi); + + quint8 depth() const; + void setDepth(quint8 depth); + +signals: + void relationshipChanged(); + void lqiChanged(); + void depthChanged(); + +private: + quint16 m_networkAddress; + ZigbeeNode::ZigbeeNodeRelationship m_relationship; + quint8 m_lqi = 0; + quint8 m_depth = 0; }; #endif // ZIGBEENODE_H diff --git a/libnymea-app/zigbee/zigbeenodes.cpp b/libnymea-app/zigbee/zigbeenodes.cpp index afd75390..a6a3aa81 100644 --- a/libnymea-app/zigbee/zigbeenodes.cpp +++ b/libnymea-app/zigbee/zigbeenodes.cpp @@ -191,3 +191,13 @@ ZigbeeNode *ZigbeeNodes::getNode(const QString &ieeeAddress) const return nullptr; } + +ZigbeeNode *ZigbeeNodes::getNodeByNetworkAddress(quint16 networkAddress) const +{ + foreach (ZigbeeNode *node, m_nodes) { + if (node->networkAddress() == networkAddress) { + return node; + } + } + return nullptr; +} diff --git a/libnymea-app/zigbee/zigbeenodes.h b/libnymea-app/zigbee/zigbeenodes.h index c28ae124..d8d1df24 100644 --- a/libnymea-app/zigbee/zigbeenodes.h +++ b/libnymea-app/zigbee/zigbeenodes.h @@ -72,6 +72,7 @@ public: Q_INVOKABLE virtual ZigbeeNode *get(int index) const; Q_INVOKABLE ZigbeeNode *getNode(const QString &ieeeAddress) const; + Q_INVOKABLE ZigbeeNode *getNodeByNetworkAddress(quint16 networkAddress) const; signals: void countChanged(); diff --git a/libnymea-app/zigbee/zigbeenodesproxy.h b/libnymea-app/zigbee/zigbeenodesproxy.h index 836a0754..61f4d994 100644 --- a/libnymea-app/zigbee/zigbeenodesproxy.h +++ b/libnymea-app/zigbee/zigbeenodesproxy.h @@ -46,6 +46,7 @@ class ZigbeeNodesProxy : public QSortFilterProxyModel Q_PROPERTY(bool showCoordinator READ showCoordinator WRITE setShowCoordinator NOTIFY showCoordinatorChanged) Q_PROPERTY(bool showOnline READ showOnline WRITE setShowOnline NOTIFY showOnlineChanged) Q_PROPERTY(bool showOffline READ showOffline WRITE setShowOffline NOTIFY showOfflineChanged) +// Q_PROPERTY(quint16 filterByParentNeighbor READ filterByParentNeighbor WRITE setFilterByParentNeighbor NOTIFY filterByParentNeighborChanged) Q_PROPERTY(bool newOnTop READ newOnTop WRITE setNewOnTop NOTIFY newOnTopChanged) @@ -89,6 +90,7 @@ private: bool m_showOffline = true; bool m_newOnTop = false; + bool m_sortByRelationship = false; QHash m_newNodes; }; diff --git a/nymea-app.pro b/nymea-app.pro index 9a372192..d2930a6f 100644 --- a/nymea-app.pro +++ b/nymea-app.pro @@ -3,7 +3,6 @@ TEMPLATE=subdirs include(shared.pri) message("APP_VERSION: $${APP_VERSION} ($${APP_REVISION})") - SUBDIRS = libnymea-app nymea-app nymea-app.depends = libnymea-app diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index f7ed3b05..87939cde 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -210,9 +210,9 @@ ui/components/ThingStatusIcons.qml ui/components/InfoPaneBase.qml ui/components/ThingInfoPane.qml - ui/system/ZigbeeSettingsPage.qml - ui/system/ZigbeeAddNetworkPage.qml - ui/system/ZigbeeNetworkSettingsPage.qml + ui/system/zigbee/ZigbeeSettingsPage.qml + ui/system/zigbee/ZigbeeAddNetworkPage.qml + ui/system/zigbee/ZigbeeNetworkSettingsPage.qml ui/MainMenu.qml ui/components/NymeaItemDelegate.qml ui/components/NymeaSwipeDelegate.qml @@ -244,7 +244,8 @@ ui/system/ModbusRtuReconfigureMasterPage.qml ui/connection/ConnectionWizard.qml ui/components/WizardPageBase.qml - ui/system/ZigbeeNetworkPage.qml + ui/system/zigbee/ZigbeeNetworkPage.qml + ui/system/zigbee/ZigbeeTopologyPage.qml ui/components/ConnectionInfoDialog.qml ui/components/ButtonControls.qml ui/components/CircleBackground.qml diff --git a/nymea-app/ui/SettingsPage.qml b/nymea-app/ui/SettingsPage.qml index 63919abe..c9327052 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -118,7 +118,7 @@ Page { text: qsTr("ZigBee") subText: qsTr("Configure ZigBee networks") visible: engine.jsonRpcClient.ensureServerVersion("5.3") && NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && Configuration.zigbeeSettingsEnabled - onClicked: pageStack.push(Qt.resolvedUrl("system/ZigbeeSettingsPage.qml")) + onClicked: pageStack.push(Qt.resolvedUrl("system/zigbee/ZigbeeSettingsPage.qml")) } SettingsTile { diff --git a/nymea-app/ui/system/ZigbeeAddNetworkPage.qml b/nymea-app/ui/system/zigbee/ZigbeeAddNetworkPage.qml similarity index 97% rename from nymea-app/ui/system/ZigbeeAddNetworkPage.qml rename to nymea-app/ui/system/zigbee/ZigbeeAddNetworkPage.qml index acc3caf7..1b854fca 100644 --- a/nymea-app/ui/system/ZigbeeAddNetworkPage.qml +++ b/nymea-app/ui/system/zigbee/ZigbeeAddNetworkPage.qml @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -33,7 +33,7 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import Nymea 1.0 -import "../components" +import "qrc:/ui/components" SettingsPageBase { id: root @@ -56,7 +56,7 @@ SettingsPageBase { ColorIcon { Layout.preferredHeight: Style.iconSize Layout.preferredWidth: Style.iconSize - name: "../images/connections/network-wifi-offline.svg" + name: "/ui/images/connections/network-wifi-offline.svg" } Label { Layout.fillWidth: true @@ -91,7 +91,7 @@ SettingsPageBase { delegate: NymeaSwipeDelegate { Layout.fillWidth: true - iconName: "../images/zigbee.svg" + iconName: "/ui/images/zigbee.svg" text: model.backend + " - " + model.description + " - " + model.serialPort onClicked: { pageStack.push(addSettingsPageComponent, {serialPort: model.serialPort, baudRate: model.baudRate, backend: model.backend, allowSerialPortSettings: false}) @@ -124,7 +124,7 @@ SettingsPageBase { delegate: NymeaSwipeDelegate { Layout.fillWidth: true property ZigbeeAdapter adapter: root.zigbeeManager.adapters.get(index) - iconName: "../images/stock_usb.svg" + iconName: "/ui/images/stock_usb.svg" text: model.description + " - " + model.serialPort onClicked: { pageStack.push(addSettingsPageComponent, {serialPort: model.serialPort, baudRate: model.baudRate, backend: model.backend, allowSerialPortSettings: true}) @@ -169,7 +169,7 @@ SettingsPageBase { default: props.errorCode = error; } - var comp = Qt.createComponent("../components/ErrorDialog.qml") + var comp = Qt.createComponent("/ui/components/ErrorDialog.qml") var popup = comp.createObject(app, props) popup.open(); } diff --git a/nymea-app/ui/system/ZigbeeNetworkPage.qml b/nymea-app/ui/system/zigbee/ZigbeeNetworkPage.qml similarity index 97% rename from nymea-app/ui/system/ZigbeeNetworkPage.qml rename to nymea-app/ui/system/zigbee/ZigbeeNetworkPage.qml index 5ea24ec6..81395ba4 100644 --- a/nymea-app/ui/system/ZigbeeNetworkPage.qml +++ b/nymea-app/ui/system/zigbee/ZigbeeNetworkPage.qml @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -32,7 +32,7 @@ import QtQuick 2.8 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.3 -import "../components" +import "qrc:/ui/components" import Nymea 1.0 SettingsPageBase { @@ -96,7 +96,7 @@ SettingsPageBase { default: props.errorCode = error; } - var comp = Qt.createComponent("../components/ErrorDialog.qml") + var comp = Qt.createComponent("/ui/components/ErrorDialog.qml") var popup = comp.createObject(app, props) popup.open(); } @@ -326,9 +326,11 @@ SettingsPageBase { additionalItem: ColorIcon { size: Style.smallIconSize anchors.verticalCenter: parent.verticalCenter - name: node.type === ZigbeeNode.ZigbeeNodeTypeRouter - ? "/ui/images/zigbee-router.svg" - : "/ui/images/zigbee-enddevice.svg" + name: node.type === ZigbeeNode.ZigbeeNodeTypeCoordinator + ? "/ui/images/zigbee-coordinator.svg" + : node.type === ZigbeeNode.ZigbeeNodeTypeRouter + ? "/ui/images/zigbee-router.svg" + : "/ui/images/zigbee-enddevice.svg" color: communicationIndicatorLedTimer.running ? Style.accentColor : Style.iconColor } diff --git a/nymea-app/ui/system/ZigbeeNetworkSettingsPage.qml b/nymea-app/ui/system/zigbee/ZigbeeNetworkSettingsPage.qml similarity index 94% rename from nymea-app/ui/system/ZigbeeNetworkSettingsPage.qml rename to nymea-app/ui/system/zigbee/ZigbeeNetworkSettingsPage.qml index 64e88a43..c2c7196b 100644 --- a/nymea-app/ui/system/ZigbeeNetworkSettingsPage.qml +++ b/nymea-app/ui/system/zigbee/ZigbeeNetworkSettingsPage.qml @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -32,7 +32,7 @@ import QtQuick 2.8 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.3 -import "../components" +import "qrc:/ui/components" import Nymea 1.0 SettingsPageBase { @@ -89,6 +89,12 @@ SettingsPageBase { progressive: false } + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Network topology") + onClicked: pageStack.push(Qt.resolvedUrl("ZigbeeTopologyPage.qml"), {network: root.network}) + } + SettingsPageSectionHeader { text: qsTr("Hardware information") } @@ -145,11 +151,11 @@ SettingsPageBase { Layout.rightMargin: app.margins text: qsTr("Remove network") onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/MeaDialog.qml")); var text = qsTr("Are you sure you want to remove the network and all associated devices from the system?") var popup = dialog.createObject(app, { - headerIcon: "../images/dialog-warning-symbolic.svg", + headerIcon: "/ui/images/dialog-warning-symbolic.svg", title: qsTr("Remove network"), text: text, standardButtons: Dialog.Ok | Dialog.Cancel @@ -168,11 +174,11 @@ SettingsPageBase { Layout.rightMargin: app.margins text: qsTr("Factory reset controller") onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/MeaDialog.qml")); var text = qsTr("Are you sure you want to factory reset the controller? This will recreate the network and remove all associated devices from the system.") var popup = dialog.createObject(app, { - headerIcon: "../images/dialog-warning-symbolic.svg", + headerIcon: "/ui/images/dialog-warning-symbolic.svg", title: qsTr("Reset controller"), text: text, standardButtons: Dialog.Ok | Dialog.Cancel diff --git a/nymea-app/ui/system/zigbee/ZigbeeNodeDelegate.qml b/nymea-app/ui/system/zigbee/ZigbeeNodeDelegate.qml new file mode 100644 index 00000000..98d73686 --- /dev/null +++ b/nymea-app/ui/system/zigbee/ZigbeeNodeDelegate.qml @@ -0,0 +1,83 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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.0 +import Nymea 1.0 +import QtQuick.Layouts 1.0 +import "qrc:/ui/components" + +ColumnLayout { + id: root + + property ZigbeeManager zigbeeManager: null + property ZigbeeNetwork zigbeeNetwork: null + property ZigbeeNode node: null + + NymeaItemDelegate { + id: thisNode + Layout.fillWidth: true + text: root.node.model + " - " + root.node.neighbors.length + } + + Repeater { + model: root.node.neighbors.length + delegate: Text { + Layout.fillWidth: true + text: "fdsfdfasa, index" + index + } + } + + Repeater { + model: root.node.neighbors.length + delegate: Loader { + id: loader + Layout.fillWidth: true + Layout.preferredHeight: item ? item.implicitHeight : 0 + source: Qt.resolvedUrl("ZigbeeNodeDelegate.qml") +// ZigbeeNodeDelegate { + Binding { + target: loader.item + property: "zigbeeManager" + value: root.zigbeeManager + } + Binding { + target: loader.item + property: "zigbeeNetwork" + value: root.zigbeeNetwork + } + + Binding { + target: loader.item + property: "node" + value: root.zigbeeNetwork.nodes.getNodeByNetworkAddress(root.node.neighbors[index].networkAddress) + } + } + } +} diff --git a/nymea-app/ui/system/ZigbeeSettingsPage.qml b/nymea-app/ui/system/zigbee/ZigbeeSettingsPage.qml similarity index 98% rename from nymea-app/ui/system/ZigbeeSettingsPage.qml rename to nymea-app/ui/system/zigbee/ZigbeeSettingsPage.qml index 4b55f72b..04e31a4e 100644 --- a/nymea-app/ui/system/ZigbeeSettingsPage.qml +++ b/nymea-app/ui/system/zigbee/ZigbeeSettingsPage.qml @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -32,7 +32,7 @@ import QtQuick 2.8 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.3 -import "../components" +import "qrc:/ui/components" import Nymea 1.0 SettingsPageBase { @@ -43,7 +43,7 @@ SettingsPageBase { onBackPressed: pageStack.pop() HeaderButton { - imageSource: "../images/add.svg" + imageSource: "/ui/images/add.svg" text: qsTr("Add ZigBee network") onClicked: { addNetwork() diff --git a/nymea-app/ui/system/zigbee/ZigbeeTopologyPage.qml b/nymea-app/ui/system/zigbee/ZigbeeTopologyPage.qml new file mode 100644 index 00000000..41de6a08 --- /dev/null +++ b/nymea-app/ui/system/zigbee/ZigbeeTopologyPage.qml @@ -0,0 +1,409 @@ +import QtQuick 2.0 +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 topology") + backButtonVisible: true + onBackPressed: pageStack.pop() + } + + property ZigbeeNetwork network: null + + readonly property int nodeDistance: 150 + readonly property int nodeSize: Style.iconSize + Style.margins + readonly property double scale: 1 + + + Component.onCompleted: { + generateNodeList() + canvas.requestPaint() + flickable.contentX = (flickable.contentWidth - flickable.width) / 2 + flickable.contentY = (flickable.contentHeight - flickable.height) / 2 + } + + function generateNodeList() { + 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 x = root.nodeDistance * Math.cos(startAngle * Math.PI / 180) + var y = root.nodeDistance * Math.sin(startAngle * Math.PI / 180) + d.nodeItems.push(createNodeItem(coordinator, x, y, startAngle)) + + var handledEndDevices = [] + + var angle = 360 / (routers.length + 1); + for (var i = 0; i < routers.length; i++) { + var router = routers[i] + var nodeAngle = startAngle + angle * (i + 1); + var x = root.nodeDistance * Math.cos(nodeAngle * Math.PI / 180) + var y = root.nodeDistance * Math.sin(nodeAngle * Math.PI / 180) + d.nodeItems.push(createNodeItem(routers[i], 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 = root.nodeDistance * 1.5 * root.scale + neighborCounter * root.nodeSize * 0.75 * root.scale + + x = neighborDistance * Math.cos(neighborAngle * Math.PI / 180) + y = neighborDistance * Math.sin(neighborAngle * Math.PI / 180) + d.nodeItems.push(createNodeItem(neighborNode, x, y, angle)) + + neighborCounter++ + } + } + } + } + + + function createNodeItem(node, x, y, angle) { + 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 + (canvas.width - Style.iconSize) / 2}), + y: Qt.binding(function() { return y + (canvas.height - Style.iconSize) / 2}), + name: icon, + color: Style.accentColor + }), + thing: thing + + } + print("creared node", thing ? thing.name : "", " at", x, y) + d.adjustSize(x, y) + return nodeItem + } + + QtObject { + id: d + property var nodeTree: ({}) + property var handledNodes: [] + + property var nodeItems: [] + + property var selectedNodeItem: 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 + var minHeight = Math.max(-minY, maxY) * 2 + size = Math.max(minWidth, minHeight) + root.nodeSize * 2 + } + + } + + Component { + id: imageComponent + ColorIcon { + } + } + + Flickable { + id: flickable + anchors.fill: parent + + contentWidth: canvas.width + contentHeight: canvas.height +// interactive: true +// flickableDirection: Flickable.HorizontalAndVerticalFlick + + Canvas { + id: canvas + width: Math.max(d.size, flickable.width) + height: Math.max(d.size, flickable.height) + clip: true + + onPaint: { + print("**** height:", canvas.height, "width", canvas.width) + 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 i = 0; i < d.nodeItems.length; i++) { + paintEdges(ctx, d.nodeItems[i], false) + } + for (var i = 0; i < d.nodeItems.length; i++) { + paintEdges(ctx, d.nodeItems[i], true) + } + for (var i = 0; i < d.nodeItems.length; i++) { + paintNode(ctx, d.nodeItems[i]) + } + } + + function paintEdges(ctx, nodeItem, selected) { + for (var i = 0; i < nodeItem.node.neighbors.length; i++) { + var neighbor = nodeItem.node.neighbors[i] + // print("ege from", nodeItem.node.networkAddress, "to", neighbor, "LQI", neighbor.lqi, "depth:", neighbor.depth) + for (var k = 0; k < d.nodeItems.length; k++) { + if (d.nodeItems[k].node.networkAddress == neighbor.networkAddress) { + var toNodeItem = d.nodeItems[k] + if (nodeItem === d.selectedNodeItem || toNodeItem === d.selectedNodeItem) { + if (selected) { + paintEdge(ctx, nodeItem, d.nodeItems[k], neighbor.lqi, true) + } + } else { + if (!selected) { + paintEdge(ctx, nodeItem, d.nodeItems[k], neighbor.lqi, false) + } + } + continue + } + } + } + } + + function paintNode(ctx, nodeItem) { + ctx.save() + ctx.beginPath(); + ctx.fillStyle = Style.tileBackgroundColor + ctx.strokeStyle = nodeItem === d.selectedNodeItem ? Style.accentColor : Style.foregroundColor + ctx.arc(root.scale * nodeItem.x, root.scale * nodeItem.y, 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, scale * (nodeItem.x - textSize.width / 2), scale * (nodeItem.y + root.nodeSize / 2 + Style.extraSmallFont.pixelSize)) + + ctx.closePath(); + + ctx.restore(); + } + + function paintEdge(ctx, fromNodeItem, toNodeItem, lqi, selected) { + ctx.save() + var percent = lqi / 255; + var goodColor = Style.green + var badColor = Style.red + var resultRed = goodColor.r + percent * (badColor.r - goodColor.r); + var resultGreen = goodColor.g + percent * (badColor.g - goodColor.g); + var resultBlue = goodColor.b + percent * (badColor.b - goodColor.b); + + if (selected) { + ctx.lineWidth = 2 + ctx.strokeStyle = Qt.rgba(resultRed, resultGreen, resultBlue, 1) + } else { + ctx.lineWidth = 1 + var alpha = d.selectedNodeItem ? .2 : 1 + ctx.strokeStyle = Qt.rgba(resultRed, resultGreen, resultBlue, alpha) + } + ctx.beginPath(); + ctx.moveTo(scale * fromNodeItem.x, scale * fromNodeItem.y) + ctx.lineTo(scale * toNodeItem.x, scale * toNodeItem.y) + + ctx.stroke(); + + ctx.closePath() + ctx.restore(); + } + + MouseArea { + anchors.fill: parent + + onClicked: { + print("clicked:", mouseX, mouseY) + var translatedMouseX = mouseX - canvas.width / 2 + var translatedMouseY = mouseY - canvas.height / 2 + d.selectedNodeItem = null + for (var i = 0; i < d.nodeItems.length; i++) { + var nodeItem = d.nodeItems[i] + // print("nodeItem at:", root.scale * nodeItem.x, root.scale * nodeItem.y) + if (Math.abs(root.scale * nodeItem.x - translatedMouseX) < (root.scale * root.nodeSize / 2) + && Math.abs(root.scale * nodeItem.y - translatedMouseY) < (root.scale * root.nodeSize / 2)) { + d.selectedNodeItem = nodeItem; + print("sleecting", nodeItem.node.networkAddress) + } + } + + canvas.requestPaint(); + } + } + } + + + } + + + BigTile { + visible: d.selectedNodeItem + anchors { + top: parent.top + right: parent.right + margins: Style.margins + } + + width: 200 + header: RowLayout { + width: parent.width - Style.smallMargins + spacing: Style.smallMargins + Label { + Layout.fillWidth: true + elide: Text.ElideRight + text: !d.selectedNodeItem + ? "" + : d.selectedNodeItem.node.networkAddress === 0 + ? Configuration.systemName + : d.selectedNodeItem.thing + ? d.selectedNodeItem.thing.name + : d.selectedNodeItem.node.model + } + ColorIcon { + size: Style.smallIconSize + name: { + if (!d.selectedNodeItem) { + return ""; + } + + var signalStrength = d.selectedNodeItem.node.lqi * 100 / 255 + if (signalStrength === 0) + 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" + } + } + ColorIcon { + size: Style.smallIconSize + name: "/ui/images/things.svg" + } + } + + contentItem: ListView { + spacing: app.margins + implicitHeight: Math.min(root.height / 4, count * Style.smallIconSize) + clip: true + model: d.selectedNodeItem ? d.selectedNodeItem.node.neighbors.length : 0 + + delegate: RowLayout { + id: neighborTableDelegate + width: parent.width + property ZigbeeNodeNeighbor neighbor: d.selectedNodeItem.node.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) + } + Label { + text: (neighborTableDelegate.neighbor.lqi * 100 / 255).toFixed(0) + "%" + font: Style.smallFont + horizontalAlignment: Text.AlignRight + } + Label { + Layout.preferredWidth: Style.smallIconSize + Style.smallMargins + font: Style.smallFont + text: neighborTableDelegate.neighbor.depth + horizontalAlignment: Text.AlignRight + } + } + + } + } + +}