Merge PR #881: Add support for configuring ZigBee bindings

This commit is contained in:
jenkins 2022-09-14 23:48:59 +02:00
commit e1f209e1de
10 changed files with 1094 additions and 25 deletions

View File

@ -329,9 +329,12 @@ 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<ZigbeeNodes>(uri, 1, 0, "ZigbeeNodes", "Get it from the ZigbeeNetwork");
qmlRegisterUncreatableType<ZigbeeNodeNeighbor>(uri, 1, 0, "ZigbeeNodeNeighbor", "Get it from the ZigbeeNode");
qmlRegisterUncreatableType<ZigbeeNodeRoute>(uri, 1, 0, "ZigbeeNodeRoute", "Get it from the ZigbeeNode");
qmlRegisterUncreatableType<ZigbeeNodes>(uri, 1, 0, "ZigbeeNodes", "Get it from the ZigbeeNetwork");
qmlRegisterUncreatableType<ZigbeeNodeBinding>(uri, 1, 0, "ZigbeeNodeBinding", "Get it from the ZigbeeNode");
qmlRegisterUncreatableType<ZigbeeNodeEndpoint>(uri, 1, 0, "ZigbeeNodeEndpoint", "Get it from the ZigbeeNode");
qmlRegisterUncreatableType<ZigbeeCluster>(uri, 1, 0, "ZigbeeCluster", "Get it from the ZigbeeNode");
qmlRegisterType<ZigbeeNodesProxy>(uri, 1, 0, "ZigbeeNodesProxy");
qmlRegisterType<ZWaveManager>(uri, 1, 0, "ZWaveManager");

View File

@ -156,6 +156,38 @@ void ZigbeeManager::refreshNeighborTables(const QUuid &networkUuid)
m_engine->jsonRpcClient()->sendCommand("Zigbee.RefreshNeighborTables", {{"networkUuid", networkUuid}});
}
int ZigbeeManager::createBinding(const QUuid &networkUuid, const QString &sourceAddress, quint8 sourceEndpointId, quint16 clusterId, const QString &destinationAddress, quint8 destinationEndpointId)
{
QVariantMap params = {
{"networkUuid", networkUuid},
{"sourceAddress", sourceAddress},
{"sourceEndpointId", sourceEndpointId},
{"clusterId", clusterId},
{"destinationAddress", destinationAddress},
{"destinationEndpointId", destinationEndpointId}
};
qCDebug(dcZigbee()) << "Creating binding for:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
return m_engine->jsonRpcClient()->sendCommand("Zigbee.CreateBinding", params, this, "createBindingResponse");
}
int ZigbeeManager::removeBinding(const QUuid &networkUuid, ZigbeeNodeBinding *binding)
{
QVariantMap params = {
{"networkUuid", networkUuid},
{"sourceAddress", binding->sourceAddress()},
{"sourceEndpointId", binding->sourceEndpointId()},
{"clusterId", binding->clusterId()}
};
if (!binding->destinationAddress().isEmpty()) {
params.insert("destinationAddress", binding->destinationAddress());
params.insert("destinationEndpointId", binding->destinationEndpointId());
} else {
params.insert("destinationGroupAddress", binding->groupAddress());
}
qCDebug(dcZigbee()) << "Removing binding for:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
return m_engine->jsonRpcClient()->sendCommand("Zigbee.RemoveBinding", params, this, "removeBindingResponse");
}
void ZigbeeManager::init()
{
m_fetchingData = true;
@ -216,7 +248,9 @@ void ZigbeeManager::getNetworksResponse(int commandId, const QVariantMap &params
void ZigbeeManager::addNetworkResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcZigbee()) << "Zigbee add network response" << commandId << params;
emit addNetworkReply(commandId, params.value("zigbeeError").toString(), params.value("networkUuid").toUuid());
QMetaEnum errorEnum = QMetaEnum::fromType<ZigbeeError>();
ZigbeeError error = static_cast<ZigbeeError>(errorEnum.keyToValue(params.value("zigbeeError").toByteArray()));
emit addNetworkReply(commandId, error, params.value("networkUuid").toUuid());
}
void ZigbeeManager::removeNetworkResponse(int commandId, const QVariantMap &params)
@ -254,7 +288,25 @@ void ZigbeeManager::getNodesResponse(int commandId, const QVariantMap &params)
void ZigbeeManager::removeNodeResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcZigbee()) << "Zigbee remove node response" << commandId << params;
emit removeNodeReply(commandId, params.value("zigbeeError").toString());
QMetaEnum errorEnum = QMetaEnum::fromType<ZigbeeError>();
ZigbeeError error = static_cast<ZigbeeError>(errorEnum.keyToValue(params.value("zigbeeError").toByteArray()));
emit removeNodeReply(commandId, error);
}
void ZigbeeManager::createBindingResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcZigbee()) << "Create binding response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
QMetaEnum errorEnum = QMetaEnum::fromType<ZigbeeError>();
ZigbeeError error = static_cast<ZigbeeError>(errorEnum.keyToValue(params.value("zigbeeError").toByteArray()));
emit createBindingReply(commandId, error);
}
void ZigbeeManager::removeBindingResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcZigbee()) << "Remove binding response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
QMetaEnum errorEnum = QMetaEnum::fromType<ZigbeeError>();
ZigbeeError error = static_cast<ZigbeeError>(errorEnum.keyToValue(params.value("zigbeeError").toByteArray()));
emit removeBindingReply(commandId, error);
}
void ZigbeeManager::notificationReceived(const QVariantMap &notification)
@ -438,4 +490,52 @@ void ZigbeeManager::updateNodeProperties(ZigbeeNode *node, const QVariantMap &no
routes.append(destinationAddress);
}
node->commitRoutes(routes);
foreach (const QVariant &binding, nodeMap.value("bindingTableRecords").toList()) {
QVariantMap bindingMap = binding.toMap();
QString sourceAddress = bindingMap.value("sourceAddress").toString();
quint8 sourceEndpointId = bindingMap.value("sourceEndpointId").toUInt();
quint16 clusterId = bindingMap.value("clusterId").toUInt();
if (bindingMap.contains("groupAddress")) {
quint16 groupAddress = bindingMap.value("groupAddress").toUInt();
node->addBinding(sourceAddress, sourceEndpointId, clusterId, groupAddress);
} else {
QString destinationAddress = bindingMap.value("destinationAddress").toString();
quint8 destinationEndpointId = bindingMap.value("destinationEndpointId").toUInt();
node->addBinding(sourceAddress, sourceEndpointId, clusterId, destinationAddress, destinationEndpointId);
}
}
node->commitBindings();
foreach (const QVariant &e, nodeMap.value("endpoints").toList()) {
QVariantMap endpointMap = e.toMap();
quint8 endpointId = endpointMap.value("endpointId").toUInt();
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(endpointId);
if (!endpoint) {
endpoint = new ZigbeeNodeEndpoint(endpointId);
node->addEndpoint(endpoint);
}
foreach (const QVariant &c, endpointMap.value("inputClusters").toList()) {
QVariantMap clusterMap = c.toMap();
quint16 clusterId = clusterMap.value("clusterId").toUInt();
if (endpoint->getInputCluster(clusterId)) {
continue;
}
QMetaEnum clusterDirectionEnum = QMetaEnum::fromType<ZigbeeCluster::ZigbeeClusterDirection>();
ZigbeeCluster::ZigbeeClusterDirection direction = static_cast<ZigbeeCluster::ZigbeeClusterDirection>(clusterDirectionEnum.keyToValue(clusterMap.value("direction").toByteArray().data()));
ZigbeeCluster *cluster = new ZigbeeCluster(clusterId, direction);
endpoint->addInputCluster(cluster);
}
foreach (const QVariant &c, endpointMap.value("outputClusters").toList()) {
QVariantMap clusterMap = c.toMap();
quint16 clusterId = clusterMap.value("clusterId").toUInt();
if (endpoint->getOutputCluster(clusterId)) {
continue;
}
QMetaEnum clusterDirectionEnum = QMetaEnum::fromType<ZigbeeCluster::ZigbeeClusterDirection>();
ZigbeeCluster::ZigbeeClusterDirection direction = static_cast<ZigbeeCluster::ZigbeeClusterDirection>(clusterDirectionEnum.keyToValue(clusterMap.value("direction").toByteArray().data()));
ZigbeeCluster *cluster = new ZigbeeCluster(clusterId, direction);
endpoint->addOutputCluster(cluster);
}
}
}

View File

@ -41,6 +41,7 @@ class ZigbeeNetwork;
class ZigbeeNetworks;
class ZigbeeNode;
class ZigbeeNodes;
class ZigbeeNodeBinding;
class ZigbeeManager : public QObject
{
@ -53,6 +54,22 @@ class ZigbeeManager : public QObject
Q_PROPERTY(ZigbeeNetworks *networks READ networks CONSTANT)
public:
enum ZigbeeError {
ZigbeeErrorNoError,
ZigbeeErrorAdapterNotAvailable,
ZigbeeErrorAdapterAlreadyInUse,
ZigbeeErrorNetworkUuidNotFound,
ZigbeeErrorDurationOutOfRange,
ZigbeeErrorNetworkOffline,
ZigbeeErrorUnknownBackend,
ZigbeeErrorNodeNotFound,
ZigbeeErrorForbidden,
ZigbeeErrorInvalidChannel,
ZigbeeErrorNetworkError,
ZigbeeErrorTimeoutError
};
Q_ENUM(ZigbeeError)
enum ZigbeeChannel {
ZigbeeChannel11 = 0x00000800,
ZigbeeChannel12 = 0x00001000,
@ -96,13 +113,17 @@ public:
Q_INVOKABLE void getNodes(const QUuid &networkUuid);
Q_INVOKABLE int removeNode(const QUuid &networkUuid, const QString &ieeeAddress);
Q_INVOKABLE void refreshNeighborTables(const QUuid &networkUuid);
Q_INVOKABLE int createBinding(const QUuid &networkUuid, const QString &sourceAddress, quint8 sourceEndpointId, quint16 clusterId, const QString &destinationAddress, quint8 destinationEndpointId);
Q_INVOKABLE int removeBinding(const QUuid &networkUuid, ZigbeeNodeBinding *binding);
signals:
void engineChanged();
void fetchingDataChanged();
void availableBackendsChanged();
void addNetworkReply(int commandId, const QString &error, const QUuid &networkUuid);
void removeNodeReply(int commandId, const QString &error);
void addNetworkReply(int commandId, ZigbeeError error, const QUuid &networkUuid);
void removeNodeReply(int commandId, ZigbeeError error);
void createBindingReply(int commandId, ZigbeeError error);
void removeBindingReply(int commandId, ZigbeeError error);
private:
void init();
@ -119,6 +140,9 @@ private:
Q_INVOKABLE void getNodesResponse(int commandId, const QVariantMap &params);
Q_INVOKABLE void removeNodeResponse(int commandId, const QVariantMap &params);
Q_INVOKABLE void createBindingResponse(int commandId, const QVariantMap &params);
Q_INVOKABLE void removeBindingResponse(int commandId, const QVariantMap &params);
Q_INVOKABLE void notificationReceived(const QVariantMap &notification);
private:

View File

@ -30,6 +30,8 @@
#include "zigbeenode.h"
#include <QMetaEnum>
ZigbeeNode::ZigbeeNode(const QUuid &networkUuid, const QString &ieeeAddress, QObject *parent) :
QObject(parent),
m_networkUuid(networkUuid),
@ -299,6 +301,81 @@ void ZigbeeNode::commitRoutes(QList<quint16> toBeKept)
}
}
QList<ZigbeeNodeBinding *> ZigbeeNode::bindings() const
{
return m_bindings;
}
void ZigbeeNode::addBinding(const QString &sourceAddress, quint8 sourceEndpointId, quint16 clusterId, quint16 groupAddress)
{
ZigbeeNodeBinding *newBinding = new ZigbeeNodeBinding(sourceAddress, sourceEndpointId, clusterId, groupAddress, this);
foreach (ZigbeeNodeBinding *binding, m_bindings) {
if (binding == newBinding) {
binding->setProperty("validated", true);
delete newBinding;
return;
}
}
newBinding->setProperty("validated", true);
m_bindings.append(newBinding);
m_bindingsDirty = true;
}
void ZigbeeNode::addBinding(const QString &sourceAddress, quint8 sourceEndpointId, quint16 clusterId, const QString &destinationAddress, quint8 destinationEndpointId)
{
ZigbeeNodeBinding *newBinding = new ZigbeeNodeBinding(sourceAddress, sourceEndpointId, clusterId, destinationAddress, destinationEndpointId, this);
foreach (ZigbeeNodeBinding *binding, m_bindings) {
if (binding == newBinding) {
binding->setProperty("validated", true);
delete newBinding;
return;
}
}
newBinding->setProperty("validated", true);
m_bindings.append(newBinding);
m_bindingsDirty = true;
}
void ZigbeeNode::commitBindings()
{
QMutableListIterator<ZigbeeNodeBinding*> it(m_bindings);
while (it.hasNext()) {
ZigbeeNodeBinding *binding = it.next();
if (!binding->property("validated").toBool()) {
it.remove();
m_bindingsDirty = true;
} else {
binding->setProperty("validated", false);
}
}
if (m_bindingsDirty) {
m_bindingsDirty = false;
emit bindingsChanged();
}
}
QList<ZigbeeNodeEndpoint *> ZigbeeNode::endpoints() const
{
return m_endpoints;
}
ZigbeeNodeEndpoint *ZigbeeNode::getEndpoint(quint8 endpointId) const
{
foreach (ZigbeeNodeEndpoint *endpoint, m_endpoints) {
if (endpoint->endpointId() == endpointId) {
return endpoint;
}
}
return nullptr;
}
void ZigbeeNode::addEndpoint(ZigbeeNodeEndpoint *endpoint)
{
endpoint->setParent(this);
m_endpoints.append(endpoint);
emit endpointsChanged();
}
ZigbeeNode::ZigbeeNodeState ZigbeeNode::stringToNodeState(const QString &nodeState)
{
if (nodeState == "ZigbeeNodeStateUninitialized") {
@ -450,3 +527,157 @@ void ZigbeeNodeRoute::setManyToOne(bool manyToOne)
emit manyToOneChanged();
}
}
ZigbeeNodeBinding::ZigbeeNodeBinding(const QString &sourceAddress, quint8 sourceEndointId, quint16 clusterId, quint16 groupAddress, QObject *parent):
QObject(parent),
m_sourceAddress(sourceAddress),
m_sourceEndpointId(sourceEndointId),
m_clusterId(clusterId),
m_type(ZigbeeNode::ZigbeeNodeBindingTypeGroup),
m_groupAddress(groupAddress)
{
}
ZigbeeNodeBinding::ZigbeeNodeBinding(const QString &sourceAddress, quint8 sourceEndointId, quint16 clusterId, const QString &destinationAddress, quint8 destinationEndpoint, QObject *parent):
QObject(parent),
m_sourceAddress(sourceAddress),
m_sourceEndpointId(sourceEndointId),
m_clusterId(clusterId),
m_type(ZigbeeNode::ZigbeeNodeBindingTypeDevice),
m_destinationAddress(destinationAddress),
m_destinationEndpointId(destinationEndpoint)
{
}
QString ZigbeeNodeBinding::sourceAddress() const
{
return m_sourceAddress;
}
quint8 ZigbeeNodeBinding::sourceEndpointId() const
{
return m_sourceEndpointId;
}
quint16 ZigbeeNodeBinding::clusterId() const
{
return m_clusterId;
}
ZigbeeNode::ZigbeeNodeBindingType ZigbeeNodeBinding::type() const
{
return m_type;
}
quint16 ZigbeeNodeBinding::groupAddress() const
{
return m_groupAddress;
}
QString ZigbeeNodeBinding::destinationAddress() const
{
return m_destinationAddress;
}
quint8 ZigbeeNodeBinding::destinationEndpointId() const
{
return m_destinationEndpointId;
}
ZigbeeCluster::ZigbeeCluster(quint16 clusterId, ZigbeeClusterDirection direction, QObject *parent):
QObject(parent),
m_clusterId(clusterId),
m_direction(direction)
{
}
quint16 ZigbeeCluster::clusterId() const
{
return m_clusterId;
}
ZigbeeCluster::ZigbeeClusterDirection ZigbeeCluster::direction() const
{
return m_direction;
}
ZigbeeNodeEndpoint::ZigbeeNodeEndpoint(quint8 endpointId, const QList<ZigbeeCluster*> &inputClusters, const QList<ZigbeeCluster*> &outputClusters, QObject *parent):
QObject(parent),
m_endpointId(endpointId),
m_inputClusters(inputClusters),
m_outputClusters(outputClusters)
{
foreach (ZigbeeCluster *cluster, inputClusters) {
cluster->setParent(this);
}
foreach (ZigbeeCluster *cluster, outputClusters) {
cluster->setParent(this);
}
}
quint8 ZigbeeNodeEndpoint::endpointId() const
{
return m_endpointId;
}
QList<ZigbeeCluster*> ZigbeeNodeEndpoint::inputClusters() const
{
return m_inputClusters;
}
ZigbeeCluster *ZigbeeNodeEndpoint::getInputCluster(quint16 clusterId) const
{
foreach (ZigbeeCluster *cluster, m_inputClusters) {
if (cluster->clusterId() == clusterId) {
return cluster;
}
}
return nullptr;
}
void ZigbeeNodeEndpoint::addInputCluster(ZigbeeCluster *cluster)
{
cluster->setParent(this);
m_inputClusters.append(cluster);
emit inputClustersChanged();
}
QList<ZigbeeCluster*> ZigbeeNodeEndpoint::outputClusters() const
{
return m_outputClusters;
}
ZigbeeCluster *ZigbeeNodeEndpoint::getOutputCluster(quint16 clusterId) const
{
foreach (ZigbeeCluster *cluster, m_outputClusters) {
if (cluster->clusterId() == clusterId) {
return cluster;
}
}
return nullptr;
}
void ZigbeeNodeEndpoint::addOutputCluster(ZigbeeCluster *cluster)
{
cluster->setParent(this);
m_outputClusters.append(cluster);
emit outputClustersChanged();
}
QString ZigbeeCluster::clusterName() const
{
QMetaEnum clusterEnum = QMetaEnum::fromType<ZigbeeClusterId>();
QString name = clusterEnum.valueToKey(m_clusterId);
name.remove("ZigbeeClusterId");
QRegExp re1 = QRegExp("([A-Z])([a-z]*)");
name.replace(re1, ";\\1\\2");
QStringList parts = name.split(";");
QString clusterName = parts.join(" ").trimmed();
if (clusterName.isEmpty()) {
clusterName = "0x" + QString::number(m_clusterId, 16);
}
return clusterName;
}

View File

@ -38,6 +38,8 @@
class ZigbeeNodeNeighbor;
class ZigbeeNodeRoute;
class ZigbeeNodeBinding;
class ZigbeeNodeEndpoint;
class ZigbeeNode : public QObject
{
@ -56,6 +58,8 @@ class ZigbeeNode : public QObject
Q_PROPERTY(QDateTime lastSeen READ lastSeen WRITE setLastSeen NOTIFY lastSeenChanged)
Q_PROPERTY(QList<ZigbeeNodeNeighbor*> neighbors READ neighbors NOTIFY neighborsChanged)
Q_PROPERTY(QList<ZigbeeNodeRoute*> routes READ routes NOTIFY routesChanged)
Q_PROPERTY(QList<ZigbeeNodeBinding*> bindings READ bindings NOTIFY bindingsChanged)
Q_PROPERTY(QList<ZigbeeNodeEndpoint*> endpoints READ endpoints NOTIFY endpointsChanged)
public:
enum ZigbeeNodeType {
@ -91,6 +95,12 @@ public:
};
Q_ENUM(ZigbeeNodeRouteStatus)
enum ZigbeeNodeBindingType {
ZigbeeNodeBindingTypeDevice,
ZigbeeNodeBindingTypeGroup
};
Q_ENUM(ZigbeeNodeBindingType)
explicit ZigbeeNode(const QUuid &networkUuid, const QString &ieeeAddress, QObject *parent = nullptr);
QUuid networkUuid() const;
@ -134,6 +144,15 @@ public:
void addOrUpdateRoute(quint16 destinationAddress, quint16 nextHopAddress, ZigbeeNodeRouteStatus status, bool memoryConstrained, bool manyToOne);
void commitRoutes(QList<quint16> toBeKept);
QList<ZigbeeNodeBinding*> bindings() const;
void addBinding(const QString &sourceAddress, quint8 sourceEndpointId, quint16 clusterId, quint16 groupAddress);
void addBinding(const QString &sourceAddress, quint8 sourceEndpointId, quint16 clusterId, const QString &destinationAddress, quint8 destinationEndpointId);
void commitBindings();
QList<ZigbeeNodeEndpoint*> endpoints() const;
Q_INVOKABLE ZigbeeNodeEndpoint *getEndpoint(quint8 endpointId) const;
void addEndpoint(ZigbeeNodeEndpoint *endpoint);
static ZigbeeNodeState stringToNodeState(const QString &nodeState);
static ZigbeeNodeType stringToNodeType(const QString &nodeType);
@ -150,6 +169,8 @@ signals:
void lastSeenChanged(const QDateTime &lastSeen);
void neighborsChanged();
void routesChanged();
void bindingsChanged();
void endpointsChanged();
private:
QUuid m_networkUuid;
@ -168,6 +189,9 @@ private:
bool m_neighborsDirty = false;
QList<ZigbeeNodeRoute*> m_routes;
bool m_routesDirty = false;
QList<ZigbeeNodeBinding*> m_bindings;
bool m_bindingsDirty = false;
QList<ZigbeeNodeEndpoint*> m_endpoints;
};
class ZigbeeNodeNeighbor: public QObject
@ -250,4 +274,184 @@ private:
bool m_manyToOne = false;
};
class ZigbeeNodeBinding: public QObject
{
Q_OBJECT
Q_PROPERTY(QString sourceAddress READ sourceAddress CONSTANT)
Q_PROPERTY(quint8 sourceEndpointId READ sourceEndpointId CONSTANT)
Q_PROPERTY(quint16 clusterId READ clusterId CONSTANT)
Q_PROPERTY(ZigbeeNode::ZigbeeNodeBindingType type READ type CONSTANT)
Q_PROPERTY(quint16 groupAddress READ groupAddress CONSTANT)
Q_PROPERTY(QString destinationAddress READ destinationAddress CONSTANT)
Q_PROPERTY(quint8 destinationEndpointId READ destinationEndpointId CONSTANT)
public:
ZigbeeNodeBinding(const QString &sourceAddress, quint8 sourceEndointId, quint16 clusterId, quint16 groupAddress, QObject *parent);
ZigbeeNodeBinding(const QString &sourceAddress, quint8 sourceEndointId, quint16 clusterId, const QString &destinationAddress, quint8 destinationEndpoint, QObject *parent);
QString sourceAddress() const;
quint8 sourceEndpointId() const;
quint16 clusterId() const;
ZigbeeNode::ZigbeeNodeBindingType type() const;
quint16 groupAddress() const;
QString destinationAddress() const;
quint8 destinationEndpointId() const;
private:
QString m_sourceAddress;
quint8 m_sourceEndpointId = 0;
quint16 m_clusterId = 0;
ZigbeeNode::ZigbeeNodeBindingType m_type = ZigbeeNode::ZigbeeNodeBindingTypeDevice;
quint16 m_groupAddress = 0;
QString m_destinationAddress;
quint8 m_destinationEndpointId = 0;
};
class ZigbeeCluster: public QObject
{
Q_OBJECT
Q_PROPERTY(quint16 clusterId READ clusterId CONSTANT)
Q_PROPERTY(ZigbeeClusterDirection direction READ direction CONSTANT)
public:
enum ZigbeeClusterDirection {
ZigbeeClusterDirectionServer,
ZigbeeClusterDirectionClient
};
Q_ENUM(ZigbeeClusterDirection)
enum ZigbeeClusterId {
// Basics
ZigbeeClusterIdUnknown = 0xffff,
ZigbeeClusterIdBasic = 0x0000,
ZigbeeClusterIdPowerConfiguration = 0x0001,
ZigbeeClusterIdDeviceTemperature = 0x0002,
ZigbeeClusterIdIdentify = 0x0003,
ZigbeeClusterIdGroups = 0x0004,
ZigbeeClusterIdScenes = 0x0005,
ZigbeeClusterIdOnOff = 0x0006,
ZigbeeClusterIdOnOffCOnfiguration = 0x0007,
ZigbeeClusterIdLevelControl = 0x0008,
ZigbeeClusterIdAlarms = 0x0009,
ZigbeeClusterIdTime = 0x000A,
ZigbeeClusterIdRssiLocation = 0x000B,
ZigbeeClusterIdAnalogInput = 0x000C,
ZigbeeClusterIdAnalogOutput = 0x000D,
ZigbeeClusterIdAnalogValue = 0x000E,
ZigbeeClusterIdBinaryInput = 0x000F,
ZigbeeClusterIdBinaryOutput = 0x0010,
ZigbeeClusterIdBinaryValue = 0x0011,
ZigbeeClusterIdMultistateInput = 0x0012,
ZigbeeClusterIdMultistateOutput = 0x0013,
ZigbeeClusterIdMultistateValue = 0x0014,
ZigbeeClusterIdCommissoning = 0x0015,
// Over the air uppgrade (OTA)
ZigbeeClusterIdOtaUpgrade = 0x0019,
// Poll controll
ZigbeeClusterIdPollControl = 0x0020,
// Closures
ZigbeeClusterIdShadeConfiguration = 0x0100,
// Door Lock
ZigbeeClusterIdDoorLock = 0x0101,
// Heating, Ventilation and Air-Conditioning (HVAC)
ZigbeeClusterIdPumpConfigurationControl = 0x0200,
ZigbeeClusterIdThermostat = 0x0201,
ZigbeeClusterIdFanControll = 0x0202,
ZigbeeClusterIdDehumiditationControl = 0x0203,
ZigbeeClusterIdThermostatUserControl = 0x0204,
// Lighting
ZigbeeClusterIdColorControl = 0x0300,
ZigbeeClusterIdBallastConfiguration = 0x0301,
// Sensing
ZigbeeClusterIdIlluminanceMeasurement = 0x0400,
ZigbeeClusterIdIlluminanceLevelSensing = 0x0401,
ZigbeeClusterIdTemperatureMeasurement = 0x0402,
ZigbeeClusterIdPressureMeasurement = 0x0403,
ZigbeeClusterIdFlowMeasurement = 0x0404,
ZigbeeClusterIdRelativeHumidityMeasurement = 0x0405,
ZigbeeClusterIdOccupancySensing = 0x0406,
// Security and Safty
ZigbeeClusterIdIasZone = 0x0500,
ZigbeeClusterIdIasAce = 0x0501,
ZigbeeClusterIdIasWd = 0x0502,
// Smart energy
ZigbeeClusterIdPrice = 0x0700,
ZigbeeClusterIdDemandResponseAndLoadControl = 0x0701,
ZigbeeClusterIdMetering = 0x0702,
ZigbeeClusterIdMessaging = 0x0703,
ZigbeeClusterIdTunneling = 0x0704,
ZigbeeClusterIdKeyEstablishment = 0x0800,
// ZLL
ZigbeeClusterIdTouchlinkCommissioning = 0x1000,
// NXP Appliances
ZigbeeClusterIdApplianceControl = 0x001B,
ZigbeeClusterIdApplianceIdentification = 0x0B00,
ZigbeeClusterIdApplianceEventsAlerts = 0x0B02,
ZigbeeClusterIdApplianceStatistics = 0x0B03,
// Electrical Measurement
ZigbeeClusterIdElectricalMeasurement = 0x0B04,
ZigbeeClusterIdDiagnostics = 0x0B05,
// Zigbee green power
ZigbeeClusterIdGreenPower = 0x0021,
// Manufacturer specific
ZigbeeClusterIdManufacturerSpecificPhilips = 0xfc00,
};
Q_ENUM(ZigbeeClusterId)
ZigbeeCluster(quint16 clusterId, ZigbeeClusterDirection direction, QObject *parent = nullptr);
quint16 clusterId() const;
ZigbeeClusterDirection direction() const;
Q_INVOKABLE QString clusterName() const;
private:
quint16 m_clusterId = 0;
ZigbeeClusterDirection m_direction;
};
class ZigbeeNodeEndpoint: public QObject
{
Q_OBJECT
Q_PROPERTY(quint8 endpointId READ endpointId CONSTANT)
Q_PROPERTY(QList<ZigbeeCluster*> inputClusters READ inputClusters NOTIFY inputClustersChanged)
Q_PROPERTY(QList<ZigbeeCluster*> outputClusters READ outputClusters NOTIFY outputClustersChanged)
public:
ZigbeeNodeEndpoint(quint8 endpointId, const QList<ZigbeeCluster*> &inputClusters = QList<ZigbeeCluster*>(), const QList<ZigbeeCluster*> &outputClusters = QList<ZigbeeCluster*>(), QObject *parent = nullptr);
quint8 endpointId() const;
QList<ZigbeeCluster*> inputClusters() const;
Q_INVOKABLE ZigbeeCluster *getInputCluster(quint16 clusterId) const;
void addInputCluster(ZigbeeCluster *cluster);
QList<ZigbeeCluster*> outputClusters() const;
Q_INVOKABLE ZigbeeCluster *getOutputCluster(quint16 clusterId) const;
void addOutputCluster(ZigbeeCluster *cluster);
signals:
void inputClustersChanged();
void outputClustersChanged();
private:
quint8 m_endpointId = 0;
QList<ZigbeeCluster*> m_inputClusters;
QList<ZigbeeCluster*> m_outputClusters;
};
#endif // ZIGBEENODE_H

View File

@ -277,5 +277,6 @@
<file>ui/system/zwave/ZWaveNetworkPage.qml</file>
<file>ui/system/zwave/ZWaveNetworkSettingsPage.qml</file>
<file>ui/components/ActivityIndicator.qml</file>
<file>ui/system/zigbee/ZigbeeNodePage.qml</file>
</qresource>
</RCC>

View File

@ -157,17 +157,17 @@ SettingsPageBase {
d.pendingCommandId = -1
var props = {};
switch (error) {
case "ZigbeeErrorNoError":
case ZigbeeManager.ZigbeeErrorNoError:
root.done()
return;
case "ZigbeeErrorAdapterNotAvailable":
case ZigbeeManager.ZigbeeErrorAdapterNotAvailable:
props.text = qsTr("The selected adapter is not available or the selected serial port configration is incorrect.");
break;
case "ZigbeeErrorAdapterAlreadyInUse":
case ZigbeeManager.ZigbeeErrorAdapterAlreadyInUse:
props.text = qsTr("The selected adapter is already in use.");
break;
default:
props.errorCode = error;
props.error = error;
}
var comp = Qt.createComponent("/ui/components/ErrorDialog.qml")
var popup = comp.createObject(app, props)

View File

@ -85,16 +85,16 @@ SettingsPageBase {
d.pendingCommandId = -1
var props = {};
switch (error) {
case "ZigbeeErrorNoError":
case ZigbeeManager.ZigbeeErrorNoError:
return;
case "ZigbeeErrorAdapterNotAvailable":
case ZigbeeManager.ZigbeeErrorAdapterNotAvailable:
props.text = qsTr("The selected adapter is not available or the selected serial port configration is incorrect.");
break;
case "ZigbeeErrorAdapterAlreadyInUse":
case ZigbeeManager.ZigbeeErrorAdapterAlreadyInUse:
props.text = qsTr("The selected adapter is already in use.");
break;
default:
props.errorCode = error;
props.error = error;
}
var comp = Qt.createComponent("/ui/components/ErrorDialog.qml")
var popup = comp.createObject(app, props)
@ -355,8 +355,9 @@ SettingsPageBase {
}
onClicked: {
var popup = nodeInfoComponent.createObject(app, {node: node, nodeThings: nodeThings})
popup.open()
pageStack.push("ZigbeeNodePage.qml", {zigbeeManager: zigbeeManager, network: network, node: node})
// var popup = nodeInfoComponent.createObject(app, {node: node, nodeThings: nodeThings})
// popup.open()
}
}
}

View File

@ -0,0 +1,502 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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.8
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.3
import "qrc:/ui/components"
import Nymea 1.0
SettingsPageBase {
id: root
property ZigbeeManager zigbeeManager: null
property ZigbeeNetwork network: null
property ZigbeeNode node: null
header: NymeaHeader {
text: qsTr("ZigBee node info")
backButtonVisible: true
onBackPressed: pageStack.pop()
HeaderButton {
imageSource: "/ui/images/delete.svg"
text: qsTr("Remove node")
onClicked: {
var popup = removeZigbeeNodeDialogComponent.createObject(app)
popup.open()
}
}
}
busy: d.pendingCommandId != -1
QtObject {
id: d
property int pendingCommandId: -1
function removeNode(networkUuid, ieeeAddress) {
d.pendingCommandId = root.zigbeeManager.removeNode(networkUuid, ieeeAddress)
}
onPendingCommandIdChanged: {
print("pendingCommandId changed", pendingCommandId, wakeupDialog)
if (pendingCommandId == -1 && wakeupDialog != null) {
wakeupDialog.close();
wakeupDialog.destroy();
wakeupDialog = null
}
}
property var wakeupDialog: null
}
Connections {
target: root.zigbeeManager
onCreateBindingReply: {
print("**** create binding reply", error)
if (commandId == d.pendingCommandId) {
d.pendingCommandId = -1
var props = {};
switch (error) {
case ZigbeeManager.ZigbeeErrorNoError:
return;
case ZigbeeManager.ZigbeeErrorNetworkError:
props.text = qsTr("An error happened in the ZigBee network when creating the binding.");
break;
case ZigbeeManager.ZigbeeErrorTimeoutError:
props.text = qsTr("The ZigBee device did not respond. Please try again.");
break;
default:
props.error = error;
}
var comp = Qt.createComponent("/ui/components/ErrorDialog.qml")
var popup = comp.createObject(app, props)
popup.open();
}
}
onRemoveBindingReply: {
if (commandId == d.pendingCommandId) {
d.pendingCommandId = -1
var props = {};
switch (error) {
case ZigbeeManager.ZigbeeErrorNoError:
return;
case ZigbeeManager.ZigbeeErrorNetworkError:
props.text = qsTr("An error happened in the ZigBee network when removing the binding.");
break;
case ZigbeeManager.ZigbeeErrorTimeoutError:
props.text = qsTr("The ZigBee device did not respond. Please try again.");
break;
default:
props.error = error;
}
var comp = Qt.createComponent("/ui/components/ErrorDialog.qml")
var popup = comp.createObject(app, props)
popup.open();
}
}
}
ThingsProxy {
id: nodeThings
engine: _engine
paramsFilter: {"ieeeAddress": root.node.ieeeAddress}
}
property int signalStrength: node ? Math.round(node.lqi * 100.0 / 255.0) : 0
NymeaItemDelegate {
Layout.fillWidth: true
text: root.node.model
subText: qsTr("Model")
prominentSubText: false
progressive: false
}
NymeaItemDelegate {
prominentSubText: false
progressive: false
Layout.fillWidth: true
text: root.node.manufacturer
subText: qsTr("Manufacturer")
}
NymeaItemDelegate {
prominentSubText: false
progressive: false
Layout.fillWidth: true
text: root.node.ieeeAddress
subText: qsTr("IEEE address")
font: Style.smallFont
}
NymeaItemDelegate {
prominentSubText: false
progressive: false
Layout.fillWidth: true
text: "0x" + root.node.networkAddress.toString(16)
subText: qsTr("Network address")
}
NymeaItemDelegate {
prominentSubText: false
progressive: false
Layout.fillWidth: true
text: (root.node.lqi * 100 / 255).toFixed(0) + " %"
subText: qsTr("Signal strength")
}
NymeaItemDelegate {
prominentSubText: false
progressive: false
Layout.fillWidth: true
text: root.node.version.length > 0 ? root.node.version : qsTr("Unknown")
subText: qsTr("Version")
}
// NymeaItemDelegate {
// Layout.fillWidth: true
// text: qsTr("Device endpoints")
// subText: qsTr("Show detailed information about the node")
// onClicked: pageStack.push(endpointsPageComponent)
// }
SettingsPageSectionHeader {
text: qsTr("Associated things")
}
Repeater {
model: nodeThings
delegate: NymeaItemDelegate {
Layout.fillWidth: true
property Thing thing: nodeThings.get(index)
iconName: app.interfacesToIcon(thing.thingClass.interfaces)
text: thing.name
onClicked: pageStack.push("/ui/thingconfiguration/ConfigureThingPage.qml", {thing: thing})
}
// delegate: RowLayout {
// id: thingDelegate
// Layout.leftMargin: Style.margins
// Layout.rightMargin: Style.margins
// property Thing thing: nodeThings.get(index)
// Layout.fillWidth: true
// ColorIcon {
// size: Style.iconSize
// source: app.interfacesToIcon(thing.thingClass.interfaces)
// color: Style.accentColor
// }
// TextField {
// text: thingDelegate.thing.name
// Layout.fillWidth: true
// onEditingFinished: engine.thingManager.editThing(thingDelegate.thing.id, text)
// }
// }
}
SettingsPageSectionHeader {
text: qsTr("Bindings")
}
property ZigbeeNode coordinatorNode: root.network.nodes.getNodeByNetworkAddress(0)
Repeater {
model: root.node.bindings
delegate: NymeaSwipeDelegate {
id: bindingDelegate
Layout.fillWidth: true
property ZigbeeNodeBinding binding: root.node.bindings[index]
ThingsProxy {
id: destinationThings
engine: _engine
paramsFilter: {"ieeeAddress": bindingDelegate.binding.destinationAddress}
}
canDelete: true
progressive: false
text: {
if (destinationThings.count > 0) {
return destinationThings.get(0).name
}
if (binding.destinationAddress != "") {
if (binding.destinationAddress == coordinatorNode.ieeeAddress) {
return Configuration.systemName
}
return binding.destinationAddress
}
return qsTr("Group: 0x%1").arg(NymeaUtils.pad(binding.groupAddress.toString(16), 4))
}
property ZigbeeNodeEndpoint endpoint: root.node.getEndpoint(binding.sourceEndpointId);
property ZigbeeCluster inputCluster: endpoint ? endpoint.getInputCluster(binding.clusterId) : null
property ZigbeeCluster outputCluster: endpoint ? endpoint.getOutputCluster(binding.clusterId) : null
property ZigbeeCluster usedCluster: inputCluster ? inputCluster : outputCluster
subText: {
var ret = usedCluster.clusterName();
if (binding.destinationAddress != "") {
ret += " (" + binding.sourceEndpointId + " -> " + binding.destinationEndpointId + ")"
}
return ret;
}
onDeleteClicked: {
if (!node.rxOnWhenIdle) {
d.wakeupDialog = wakeupDialogComponent.createObject(root)
d.wakeupDialog.open()
}
d.pendingCommandId = zigbeeManager.removeBinding(network.networkUuid, binding)
}
}
}
Button {
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
text: qsTr("Add binding")
onClicked: {
var dialog = addBindingComponent.createObject(root)
dialog.open()
}
}
Component {
id: removeZigbeeNodeDialogComponent
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: endpointsPageComponent
SettingsPageBase {
title: qsTr("Device endpoints")
Repeater {
model: root.node.endpoints
delegate: ColumnLayout {
id: endpointDelegate
property ZigbeeNodeEndpoint endpoint: root.node.endpoints[index]
Label {
Layout.fillWidth: true
text: "- " + qsTr("Endpoint %1").arg(endpointDelegate.endpoint.endpointId)
}
Label {
Layout.fillWidth: true
text: " " + qsTr("Input clusters")
}
Repeater {
model: endpointDelegate.endpoint.inputClusters
delegate: Label {
Layout.fillWidth: true
property ZigbeeCluster cluster: endpointDelegate.endpoint.inputClusters[index]
text: " - " + cluster.clusterName() + " (" + (cluster.direction == ZigbeeCluster.ZigbeeClusterDirectionClient ? qsTr("Client") : qsTr("Server")) + ")"
}
}
Label {
Layout.fillWidth: true
text: " " + qsTr("Output clusters")
}
Repeater {
model: endpointDelegate.endpoint.outputClusters
delegate: Label {
Layout.fillWidth: true
property ZigbeeCluster cluster: endpointDelegate.endpoint.outputClusters[index]
text: " - " + cluster.clusterName() + " (" + (cluster.direction == ZigbeeCluster.ZigbeeClusterDirectionClient ? qsTr("Client") : qsTr("Server")) + ")"
}
}
}
}
}
}
Component {
id: addBindingComponent
MeaDialog {
title: qsTr("Add binding")
Label {
text: qsTr("Source endpoint")
Layout.fillWidth: true
}
ComboBox {
Layout.fillWidth: true
id: sourceEndpointComboBox
model: root.node.endpoints
textRole: "endpointId"
displayText: currentEndpoint.endpointId
property ZigbeeNodeEndpoint currentEndpoint: root.node.endpoints[currentIndex]
onCurrentEndpointChanged: print("source endpoint changed", currentEndpoint.endpointId)
}
Label {
text: qsTr("Target node")
Layout.fillWidth: true
}
ComboBox {
id: destinationNodeComboBox
Layout.fillWidth: true
model: network.nodes
displayText: currentDestinationNodeThings.count > 0
? currentDestinationNodeThings.get(0).name
: currentNode == root.coordinatorNode
? Configuration.systemName
: currentNode.model
property ZigbeeNode currentNode: network.nodes.get(currentIndex)
ThingsProxy {
id: currentDestinationNodeThings
engine: _engine
paramsFilter: {"ieeeAddress": destinationNodeDelegate.node.ieeeAddress}
}
delegate: ItemDelegate {
id: destinationNodeDelegate
property ZigbeeNode node: network.nodes.get(index)
width: parent.width
text: destinationNodeThings.count > 0
? destinationNodeThings.get(0).name
: node == root.coordinatorNode
? Configuration.systemName
: node.model
ThingsProxy {
id: destinationNodeThings
engine: _engine
paramsFilter: {"ieeeAddress": destinationNodeDelegate.node.ieeeAddress}
}
}
}
Label {
text: qsTr("Destination endpoint")
Layout.fillWidth: true
}
ComboBox {
id: destinationEndpointComboBox
Layout.fillWidth: true
model: destinationNodeComboBox.currentNode.endpoints
textRole: "endpointId"
displayText: currentEndpoint.endpointId
property ZigbeeNodeEndpoint currentEndpoint: destinationNodeComboBox.currentNode.endpoints[currentIndex]
}
Label {
text: qsTr("Cluster")
Layout.fillWidth: true
}
ComboBox {
id: clusterComboBox
Layout.fillWidth: true
delegate: ItemDelegate {
width: parent.width
text: modelData.clusterName()
}
model: {
var ret = []
print("updating clusters", sourceEndpointComboBox.currentEndpoint, destinationNodeComboBox.currentNode, destinationEndpointComboBox.currentEndpoint)
if (!sourceEndpointComboBox.currentEndpoint || !destinationNodeComboBox.currentNode || !destinationEndpointComboBox.currentEndpoint) {
return ret;
}
for (var i = 0; i < sourceEndpointComboBox.currentEndpoint.outputClusters.length; i++) {
var outputCluster = sourceEndpointComboBox.currentEndpoint.outputClusters[i]
print("source has cluster", outputCluster.clusterId)
for (var j = 0; j < destinationEndpointComboBox.currentEndpoint.inputClusters.length; j++) {
var inputCluster = destinationEndpointComboBox.currentEndpoint.inputClusters[j]
print("destination has cluster", inputCluster.clusterId)
if (inputCluster.clusterId === outputCluster.clusterId && inputCluster.direction !== outputCluster.direction) {
ret.push(outputCluster);
break;
}
}
}
for (var i = 0; i < sourceEndpointComboBox.currentEndpoint.inputClusters.length; i++) {
var inputCluster = sourceEndpointComboBox.currentEndpoint.inputClusters[i]
print("source has cluster", inputCluster.clusterId)
for (var j = 0; j < destinationEndpointComboBox.currentEndpoint.outputClusters.length; j++) {
var outputCluster = destinationEndpointComboBox.currentEndpoint.outputClusters[j]
print("destination has cluster", outputCluster.clusterId)
if (inputCluster.clusterId === outputCluster.clusterId && inputCluster.direction !== outputCluster.direction) {
ret.push(inputCluster);
break;
}
}
}
return ret
}
property ZigbeeCluster currentCluster: currentValue
displayText: currentValue.clusterName()
}
onAccepted: {
d.pendingCommandId = root.zigbeeManager.createBinding(
root.network.networkUuid,
root.node.ieeeAddress,
sourceEndpointComboBox.currentEndpoint.endpointId,
clusterComboBox.currentCluster.clusterId,
destinationNodeComboBox.currentNode.ieeeAddress,
destinationEndpointComboBox.currentEndpoint.endpointId)
if (!root.node.rxOnWhenIdle) {
d.wakeupDialog = wakeupDialogComponent.createObject(root)
d.wakeupDialog.open()
}
}
}
}
Component {
id: wakeupDialogComponent
MeaDialog {
id: wakeupDialog
title: qsTr("Wake up %1").arg(root.node.model)
text: qsTr("The selected node is a sleepy device. Please wake up the device by pressing a button.")
}
}
}

View File

@ -121,23 +121,26 @@ SettingsPageBase {
text: qsTr("Information")
}
NymeaSwipeDelegate {
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Vendor:")
subText: engine.thingManager.vendors.getVendor(root.thing.thingClass.vendorId).displayName
text: engine.thingManager.vendors.getVendor(root.thing.thingClass.vendorId).displayName
subText: qsTr("Vendor")
prominentSubText: false
progressive: false
}
NymeaSwipeDelegate {
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Type:")
subText: root.thing.thingClass.displayName
text: root.thing.thingClass.displayName
subText: qsTr("Type")
prominentSubText: false
progressive: false
}
NymeaSwipeDelegate {
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("ID:")
subText: root.thing.id.toString().replace(/[{}]/g, "")
text: root.thing.id.toString().replace(/[{}]/g, "")
subText: qsTr("ID")
prominentSubText: false
progressive: false
onClicked: {
PlatformHelper.toClipBoard(root.thing.id.toString().replace(/[{}]/g, ""));
@ -145,7 +148,7 @@ SettingsPageBase {
}
}
NymeaSwipeDelegate {
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Thing class")
subText: qsTr("View the type definition for this thing")