Add a zigbee network topology map

This commit is contained in:
Michael Zanetti 2022-09-01 12:04:51 +02:00
parent 8876a8635a
commit a7a3c15abe
19 changed files with 722 additions and 50 deletions

View File

@ -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) {

View File

@ -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<QSslError> &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

View File

@ -329,6 +329,7 @@ void registerQmlTypes() {
qmlRegisterUncreatableType<ZigbeeNetwork>(uri, 1, 0, "ZigbeeNetwork", "Get it from the ZigbeeManager");
qmlRegisterUncreatableType<ZigbeeNetworks>(uri, 1, 0, "ZigbeeNetworks", "Get it from the ZigbeeManager");
qmlRegisterUncreatableType<ZigbeeNode>(uri, 1, 0, "ZigbeeNode", "Get it from the ZigbeeNodes");
qmlRegisterUncreatableType<ZigbeeNodeNeighbor>(uri, 1, 0, "ZigbeeNodeNeighbor", "Get it from the ZigbeeNode");
qmlRegisterUncreatableType<ZigbeeNodes>(uri, 1, 0, "ZigbeeNodes", "Get it from the ZigbeeNetwork");
qmlRegisterType<ZigbeeNodesProxy>(uri, 1, 0, "ZigbeeNodesProxy");

View File

@ -231,7 +231,7 @@ void ZigbeeManager::factoryResetNetworkResponse(int commandId, const QVariantMap
void ZigbeeManager::getNodesResponse(int commandId, const QVariantMap &params)
{
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 &params)
void ZigbeeManager::notificationReceived(const QVariantMap &notification)
{
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<quint16> 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>();
ZigbeeNode::ZigbeeNodeRelationship relationship = static_cast<ZigbeeNode::ZigbeeNodeRelationship>(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);
}

View File

@ -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

View File

@ -189,6 +189,51 @@ void ZigbeeNode::setLastSeen(const QDateTime &lastSeen)
emit lastSeenChanged(m_lastSeen);
}
QList<ZigbeeNodeNeighbor *> 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<quint16> toBeKept)
{
QMutableListIterator<ZigbeeNodeNeighbor*> 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();
}
}

View File

@ -36,6 +36,8 @@
#include <QDateTime>
#include <QVariantMap>
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<ZigbeeNodeNeighbor*> 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<ZigbeeNodeNeighbor*> neighbors() const;
void addOrUpdateNeighbor(quint16 networkAddress, ZigbeeNodeRelationship relationship, quint8 lqi, quint8 depth);
void commitNeighbors(QList<quint16> 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<ZigbeeNodeNeighbor*> 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

View File

@ -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;
}

View File

@ -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();

View File

@ -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<ZigbeeNode*, QDateTime> m_newNodes;
};

View File

@ -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

View File

@ -210,9 +210,9 @@
<file>ui/components/ThingStatusIcons.qml</file>
<file>ui/components/InfoPaneBase.qml</file>
<file>ui/components/ThingInfoPane.qml</file>
<file>ui/system/ZigbeeSettingsPage.qml</file>
<file>ui/system/ZigbeeAddNetworkPage.qml</file>
<file>ui/system/ZigbeeNetworkSettingsPage.qml</file>
<file>ui/system/zigbee/ZigbeeSettingsPage.qml</file>
<file>ui/system/zigbee/ZigbeeAddNetworkPage.qml</file>
<file>ui/system/zigbee/ZigbeeNetworkSettingsPage.qml</file>
<file>ui/MainMenu.qml</file>
<file>ui/components/NymeaItemDelegate.qml</file>
<file>ui/components/NymeaSwipeDelegate.qml</file>
@ -244,7 +244,8 @@
<file>ui/system/ModbusRtuReconfigureMasterPage.qml</file>
<file>ui/connection/ConnectionWizard.qml</file>
<file>ui/components/WizardPageBase.qml</file>
<file>ui/system/ZigbeeNetworkPage.qml</file>
<file>ui/system/zigbee/ZigbeeNetworkPage.qml</file>
<file>ui/system/zigbee/ZigbeeTopologyPage.qml</file>
<file>ui/components/ConnectionInfoDialog.qml</file>
<file>ui/components/ButtonControls.qml</file>
<file>ui/components/CircleBackground.qml</file>

View File

@ -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 {

View File

@ -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();
}

View File

@ -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
}

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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)
}
}
}
}

View File

@ -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()

View File

@ -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
}
}
}
}
}