#include "mqttitem.h" #include #include #include #include #include "mqttclient.h" #include "programmode.h" MqttItem::MqttItem(QString name, uint8_t value, QObject *parent) : Item(0, name, value, parent), topic_(""), valueKey_("state"), valueOn_("ON"), valueOff_("OFF") { hashId(); std::shared_ptr workClient = client.lock(); assert(workClient || programMode == PROGRAM_MODE_UI_ONLY); if(workClient) connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged); } MqttItem::~MqttItem() { qDebug()<<__func__; std::shared_ptr workClient = client.lock(); if(!workClient) return; 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() { std::shared_ptr workClient = client.lock(); if(!workClient || topic_.isEmpty()) return; if(workClient->getClient()->state() != QMqttClient::Connected) return; if(subscription) { disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived); workClient->unsubscribe(subscription); } subscription = workClient->subscribe(workClient->getBaseTopic() + "/" + getTopic()); 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; Q_EMIT exposeLoaded(); // 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()); if(doc.isObject()) { QJsonObject obj = doc.object(); if(obj.contains(getValueKey())) { QJsonValue value = obj[getValueKey()]; ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND); req.changes.value = 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 { // Binary value QString strValue = value.toString(); if(strValue == getValueOn() || strValue == "ON" || strValue == "true") req.payload.setValueData(true); else req.payload.setValueData(false); } requestUpdate(req); } } } void MqttItem::hashId() { QString hashString = topic_ + "/" + valueKey_; itemId_ = qHash(hashString.toLatin1()); } void MqttItem::setTopic(const QString& topic) { topic_ = topic; hashId(); refreshSubscription(); } void MqttItem::setValueKey(const QString& valueKey) { valueKey_ = valueKey; hashId(); } void MqttItem::setValueOn(const QString& valueOn) { valueOn_ = valueOn; } 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(); } void MqttItem::triggerExposeLookup() { if(exposeLoaded_) return; std::shared_ptr workClient = client.lock(); if(!workClient) return; // Reset expose loaded flag to allow re-detection exposeLoaded_ = false; // Subscribe to bridge/devices if(devicesSubscription) { disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived); workClient->unsubscribe(devicesSubscription); devicesSubscription = nullptr; } devicesSubscription = workClient->subscribe(workClient->getBaseTopic() + "/bridge/devices"); connect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived); } QString MqttItem::getTopic() const { return topic_; } QString MqttItem::getValueKey() const { return valueKey_; } QString MqttItem::getValueOn() const { return valueOn_; } 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); json["Type"] = "Mqtt"; json["Topic"] = topic_; 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) { Item::load(json, preserve); topic_ = json["Topic"].toString(); 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(); } void MqttItem::enactValue(uint8_t value) { std::shared_ptr workClient = client.lock(); if(!workClient || topic_.isEmpty()) return; QString fullTopic = workClient->getBaseTopic() + "/" + topic_ + "/set"; QJsonObject payload; 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); qDebug() << "MqttItem publishing to" << fullTopic << ":" << data; workClient->getClient()->publish(fullTopic, data); }