From 05e4fb1cc180d690989a7f695b4948e7af7053f4 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Fri, 17 Apr 2026 18:35:16 +0200 Subject: [PATCH] Auto mqttitem attempt --- src/items/mqttitem.cpp | 239 +++++++++++++++++++++++++++++++++++++++-- src/items/mqttitem.h | 23 +++- src/mqttclient.cpp | 3 +- 3 files changed, 254 insertions(+), 11 deletions(-) diff --git a/src/items/mqttitem.cpp b/src/items/mqttitem.cpp index 76c5264..9c2ea78 100644 --- a/src/items/mqttitem.cpp +++ b/src/items/mqttitem.cpp @@ -1,5 +1,6 @@ #include "mqttitem.h" +#include #include #include #include @@ -26,16 +27,35 @@ MqttItem::~MqttItem() { qDebug()<<__func__; std::shared_ptr workClient = client.lock(); - if(!workClient || topic_.isEmpty() || !subscription) + if(!workClient) return; - workClient->unsubscribe(subscription); + if(subscription) + { + disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived); + workClient->unsubscribe(subscription); + } + + if(devicesSubscription) + { + disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived); + workClient->unsubscribe(devicesSubscription); + } } void MqttItem::onClientStateChanged(QMqttClient::ClientState state) { if(state == QMqttClient::Connected) + { refreshSubscription(); + // Subscribe to bridge/devices to get exposes + std::shared_ptr workClient = client.lock(); + if(workClient && !exposeLoaded_) + { + devicesSubscription = workClient->subscribe(workClient->getBaseTopic() + "/bridge/devices"); + connect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived); + } + } } void MqttItem::refreshSubscription() @@ -57,6 +77,101 @@ void MqttItem::refreshSubscription() connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived); } +void MqttItem::onDevicesMessageReceived(const QMqttMessage& message) +{ + if(exposeLoaded_) + return; + + QJsonDocument doc = QJsonDocument::fromJson(message.payload()); + if(!doc.isArray()) + return; + + QJsonArray devices = doc.array(); + for(const QJsonValue& deviceValue : devices) + { + if(!deviceValue.isObject()) + continue; + + QJsonObject device = deviceValue.toObject(); + QString ieeeAddr = device["ieee_address"].toString(); + + // Check if this device matches our topic (friendly_name) + QString friendlyName = device["friendly_name"].toString(); + if(friendlyName == topic_) + { + loadExposeFromDevice(device); + exposeLoaded_ = true; + + // Unsubscribe from devices topic since we found our device + std::shared_ptr workClient = client.lock(); + if(workClient && devicesSubscription) + { + disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived); + workClient->unsubscribe(devicesSubscription); + devicesSubscription = nullptr; + } + break; + } + } +} + +void MqttItem::loadExposeFromDevice(const QJsonObject& device) +{ + // Get definition - may be null for unsupported devices + QJsonObject definition = device["definition"].toObject(); + if(definition.isEmpty()) + { + qWarning() << "MqttItem" << topic_ << "device has no definition (unsupported)"; + return; + } + + // Get exposes from definition + QJsonArray exposes = definition["exposes"].toArray(); + if(exposes.isEmpty()) + { + qWarning() << "MqttItem" << topic_ << "device has no exposes"; + return; + } + + for(const QJsonValue& exposeValue : exposes) + { + if(!exposeValue.isObject()) + continue; + + QJsonObject expose = exposeValue.toObject(); + QString property = expose["property"].toString(); + + // Check if this expose matches our valueKey + if(property == valueKey_) + { + setFromExpose(expose); + qDebug() << "MqttItem" << topic_ << "detected type" << expose["type"].toString() << "for property" << valueKey_; + return; + } + + // Check if it's a composite type with features + if(expose["type"].toString() == "composite" || expose["type"].toString() == "light") + { + QJsonArray features = expose["features"].toArray(); + for(const QJsonValue& featureValue : features) + { + if(!featureValue.isObject()) + continue; + + QJsonObject feature = featureValue.toObject(); + if(feature["property"].toString() == valueKey_) + { + setFromExpose(feature); + qDebug() << "MqttItem" << topic_ << "detected type" << feature["type"].toString() << "for property" << valueKey_; + return; + } + } + } + } + + qWarning() << "MqttItem" << topic_ << "could not find expose for property" << valueKey_; +} + void MqttItem::onMessageReceived(const QMqttMessage& message) { QJsonDocument doc = QJsonDocument::fromJson(message.payload()); @@ -65,13 +180,32 @@ void MqttItem::onMessageReceived(const QMqttMessage& message) QJsonObject obj = doc.object(); if(obj.contains(getValueKey())) { - QString value = obj[getValueKey()].toString(); + QJsonValue value = obj[getValueKey()]; ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND); req.changes.value = true; - if(value == getValueOn()) - req.payload.setValueData(true); + + if(getValueType() == ITEM_VALUE_UINT) + { + // Numeric value + req.payload.setValueData(value.toInt(0)); + } + else if(getValueType() == ITEM_VALUE_ENUM) + { + // Enum value - find index + QString strValue = value.toString(); + int index = valueNameToIndex(strValue); + if(index >= 0) + req.payload.setValueData(index); + } else - req.payload.setValueData(false); + { + // Binary value + QString strValue = value.toString(); + if(strValue == getValueOn() || strValue == "ON" || strValue == "true") + req.payload.setValueData(true); + else + req.payload.setValueData(false); + } requestUpdate(req); } } @@ -106,6 +240,59 @@ void MqttItem::setValueOff(const QString& valueOff) valueOff_ = valueOff; } +void MqttItem::setValueMin(int min) +{ + valueMin_ = min; +} + +void MqttItem::setValueMax(int max) +{ + valueMax_ = max; +} + +void MqttItem::setValueStep(int step) +{ + valueStep_ = step; +} + +void MqttItem::setValueType(item_value_type_t type) +{ + type_ = type; +} + +void MqttItem::setFromExpose(const QJsonObject& expose) +{ + QString type = expose["type"].toString(); + QString property = expose["property"].toString(); + + setValueKey(property); + + if(type == "binary") + { + type_ = ITEM_VALUE_BOOL; + setValueOn(expose["value_on"].toString("ON")); + setValueOff(expose["value_off"].toString("OFF")); + } + else if(type == "numeric") + { + type_ = ITEM_VALUE_UINT; + setValueMin(expose["value_min"].toInt(0)); + setValueMax(expose["value_max"].toInt(255)); + setValueStep(expose["value_step"].toInt(1)); + } + else if(type == "enum") + { + type_ = ITEM_VALUE_ENUM; + QJsonArray values = expose["values"].toArray(); + std::vector valueNames; + for(const QJsonValue& v : values) + valueNames.push_back(v.toString()); + setValueNames(valueNames); + } + + hashId(); +} + QString MqttItem::getTopic() const { return topic_; @@ -126,6 +313,26 @@ QString MqttItem::getValueOff() const return valueOff_; } +int MqttItem::getValueMin() const +{ + return valueMin_; +} + +int MqttItem::getValueMax() const +{ + return valueMax_; +} + +int MqttItem::getValueStep() const +{ + return valueStep_; +} + +bool MqttItem::getExposeLoaded() const +{ + return exposeLoaded_; +} + void MqttItem::store(QJsonObject& json) { Item::store(json); @@ -134,6 +341,9 @@ void MqttItem::store(QJsonObject& json) json["ValueKey"] = valueKey_; json["ValueOn"] = valueOn_; json["ValueOff"] = valueOff_; + json["ValueMin"] = valueMin_; + json["ValueMax"] = valueMax_; + json["ValueStep"] = valueStep_; } void MqttItem::load(const QJsonObject& json, const bool preserve) @@ -143,6 +353,10 @@ void MqttItem::load(const QJsonObject& json, const bool preserve) valueKey_ = json["ValueKey"].toString("state"); valueOn_ = json["ValueOn"].toString("ON"); valueOff_ = json["ValueOff"].toString("OFF"); + valueMin_ = json["ValueMin"].toInt(0); + valueMax_ = json["ValueMax"].toInt(255); + valueStep_ = json["ValueStep"].toInt(1); + exposeLoaded_ = json["ExposeLoaded"].toBool(false); hashId(); refreshSubscription(); } @@ -156,7 +370,18 @@ void MqttItem::enactValue(uint8_t value) QString fullTopic = workClient->getBaseTopic() + "/" + topic_ + "/set"; QJsonObject payload; - payload[valueKey_] = value ? valueOn_ : valueOff_; + if(getValueType() == ITEM_VALUE_UINT) + { + payload[valueKey_] = static_cast(value); + } + else if(getValueType() == ITEM_VALUE_ENUM) + { + payload[valueKey_] = indexToValueName(value); + } + else + { + payload[valueKey_] = value ? valueOn_ : valueOff_; + } QJsonDocument doc(payload); QByteArray data = doc.toJson(QJsonDocument::Compact); diff --git a/src/items/mqttitem.h b/src/items/mqttitem.h index c092444..f31203c 100644 --- a/src/items/mqttitem.h +++ b/src/items/mqttitem.h @@ -15,15 +15,22 @@ public: private: QString topic_; QString valueKey_; - QString valueOn_; - QString valueOff_; + QString valueOn_ = "ON"; + QString valueOff_ = "OFF"; + int valueMin_ = 0; + int valueMax_ = 255; + int valueStep_ = 1; + bool exposeLoaded_ = false; MqttClient::Subscription* subscription = nullptr; + MqttClient::Subscription* devicesSubscription = nullptr; void hashId(); void refreshSubscription(); void onMessageReceived(const QMqttMessage& message); void onClientStateChanged(QMqttClient::ClientState state); + void onDevicesMessageReceived(const QMqttMessage& message); + void loadExposeFromDevice(const QJsonObject& device); public: explicit MqttItem(QString name = "MqttItem", @@ -36,12 +43,22 @@ public: void setBaseTopic(const QString& baseTopic); void setValueOn(const QString& valueOn); void setValueOff(const QString& valueOff); + void setValueMin(int min); + void setValueMax(int max); + void setValueStep(int step); + void setValueType(item_value_type_t type); + + // Configure from Zigbee2MQTT expose info + void setFromExpose(const QJsonObject& expose); QString getTopic() const; QString getValueKey() const; - QString getBaseTopic() const; QString getValueOn() const; QString getValueOff() const; + int getValueMin() const; + int getValueMax() const; + int getValueStep() const; + bool getExposeLoaded() const; virtual void store(QJsonObject& json) override; virtual void load(const QJsonObject& json, const bool preserve = false) override; diff --git a/src/mqttclient.cpp b/src/mqttclient.cpp index ab61375..eb504a6 100644 --- a/src/mqttclient.cpp +++ b/src/mqttclient.cpp @@ -16,11 +16,12 @@ void MqttClient::start(const QJsonObject& settings) client->setHostname(settings["Host"].toString("127.0.0.1")); client->setPort(settings["Port"].toInt(1883)); + client->setClientId(settings["ClientId"].toString("smartvos")); if(settings.contains("User")) client->setUsername(settings["User"].toString()); if(settings.contains("Password")) client->setPassword(settings["Password"].toString()); - client->setProtocolVersion(QMqttClient::MQTT_5_0); + client->setProtocolVersion(QMqttClient::MQTT_3_1); client->connectToHost(); }