From 58ba22b26780d99417e24e73589570bb81eed430 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Fri, 19 Jun 2026 12:15:41 +0200 Subject: [PATCH] Fix mqtt item expose detection only working on local instances --- CMakeLists.txt | 5 +- src/items/item.cpp | 19 +- src/items/item.h | 24 +- src/items/itemloadersource.cpp | 1 - src/items/itemstore.cpp | 2 +- src/items/mqttitem.cpp | 148 +++++------ src/items/mqttitem.h | 20 +- src/mainobject.cpp | 3 + src/microcontroller.cpp | 6 +- src/mqttclient.cpp | 26 +- src/mqttclient.h | 5 + src/sensors/sensor.cpp | 34 ++- src/sensors/sensor.h | 3 + src/service/server.cpp | 16 +- src/service/server.h | 1 + src/service/service.cpp | 24 +- src/service/service.h | 2 + src/service/tcpclient.cpp | 1 - .../mqttitemsettingswidget.cpp | 114 ++++++--- .../mqttitemsettingswidget.h | 12 +- src/ui/itemwidget.cpp | 31 ++- src/ui/itemwidget.h | 7 + tests/unit/items/test_item.cpp | 9 - tests/unit/items/test_mqttitem.cpp | 242 +----------------- 24 files changed, 340 insertions(+), 415 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c8fd913..b077d14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,12 @@ project(smartvos VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Enable all warnings add_compile_options(-Wall) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "/usr" CACHE PATH "..." FORCE) +endif() + # Find Qt packages find_package(Qt6 COMPONENTS Core Gui Widgets Network Multimedia SerialPort Mqtt WebSockets REQUIRED) diff --git a/src/items/item.cpp b/src/items/item.cpp index 6b2eb5b..da7e446 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -14,8 +14,8 @@ #include -ItemData::ItemData(uint32_t itemIdIn, QString name, uint8_t value, bool loaded, bool hidden, item_value_type_t type, QString groupName): - name_(name), value_(value), itemId_(itemIdIn), loaded_(loaded), hidden_(hidden), type_(type), groupName_(groupName) +ItemData::ItemData(uint32_t itemIdIn, QString name, uint8_t value, bool hidden, item_value_type_t type, QString groupName): + name_(name), value_(value), itemId_(itemIdIn), hidden_(hidden), type_(type), groupName_(groupName) { } @@ -119,16 +119,6 @@ ItemFieldChanges ItemData::loadWithChanges(const QJsonObject& json, const bool p return changes; } -bool ItemData::getLoaded() const -{ - return loaded_; -} - -void ItemData::setLoaded(bool loaded) -{ - loaded_ = loaded; -} - bool ItemData::hasChanged(const ItemData& other) const { ItemFieldChanges changes(true); @@ -219,7 +209,7 @@ void ItemData::setOverride(bool overrideVal) //item Item::Item(uint32_t itemIdIn, QString name, uint8_t value, QObject *parent): QObject(parent), ItemData (itemIdIn, name, - value, false, false, ITEM_VALUE_BOOL, "All") + value, false, ITEM_VALUE_BOOL, "All") { } @@ -421,10 +411,7 @@ std::shared_ptr Item::loadItem(const QJsonObject& json) else qWarning()<<"Unable to load unkown item type: "<load(json); - newItem->setLoaded(true); - } return newItem; } diff --git a/src/items/item.h b/src/items/item.h index 6ddf6ef..38fa979 100644 --- a/src/items/item.h +++ b/src/items/item.h @@ -24,6 +24,26 @@ typedef enum { ITEM_UPDATE_INVALID } item_update_type_t; + +inline static const char* itemUpdateTypeToString(item_update_type_t type) +{ + switch(type) + { + case ITEM_UPDATE_USER: + return "User"; + case ITEM_UPDATE_ACTOR: + return "Actor"; + case ITEM_UPDATE_REMOTE: + return "Remote"; + case ITEM_UPDATE_LOADED: + return "Loaded"; + case ITEM_UPDATE_BACKEND: + return "Backend"; + default: + return "Invalid"; + } +} + struct ItemFieldChanges; struct ItemUpdateRequest; @@ -33,7 +53,6 @@ protected: QString name_; uint8_t value_; uint32_t itemId_; - bool loaded_; bool hidden_; item_value_type_t type_; QString groupName_; @@ -44,7 +63,6 @@ public: ItemData(uint32_t itemIdIn = QRandomGenerator::global()->generate(), QString name = "Item", uint8_t value = 0, - bool loaded = false, bool hidden = false, item_value_type_t type = ITEM_VALUE_BOOL, QString groupName = ""); @@ -65,8 +83,6 @@ public: void setName(QString name); uint8_t getValue() const; void setValueData(uint8_t value); - bool getLoaded() const; - void setLoaded(bool loaded); bool isHidden() const; void setHidden(bool hidden); item_value_type_t getValueType(); diff --git a/src/items/itemloadersource.cpp b/src/items/itemloadersource.cpp index b689a3c..ec24089 100644 --- a/src/items/itemloadersource.cpp +++ b/src/items/itemloadersource.cpp @@ -21,7 +21,6 @@ void ItemLoaderSource::refresh() std::shared_ptr newItem = Item::loadItem(itemObject); if(newItem) { - qDebug()<<"Loaded item"<getName(); ItemAddRequest request; request.type = ITEM_UPDATE_LOADED; request.payload = newItem; diff --git a/src/items/itemstore.cpp b/src/items/itemstore.cpp index 1b52bf9..747cbba 100644 --- a/src/items/itemstore.cpp +++ b/src/items/itemstore.cpp @@ -23,7 +23,7 @@ void ItemStore::addItem(const ItemAddRequest& item) { items_.push_back(item.payload); connect(item.payload.get(), &Item::updated, this, &ItemStore::itemUpdateSlot); - qDebug()<<"Item"<getName()<<"added"<<(item.payload->getLoaded() ? "from loaded" : ""); + qDebug()<<"Item"<getName()<<"added with id"<id()<<"from"<(items_.back())); } else if(!item.changes.isNone()) diff --git a/src/items/mqttitem.cpp b/src/items/mqttitem.cpp index da91bef..217d811 100644 --- a/src/items/mqttitem.cpp +++ b/src/items/mqttitem.cpp @@ -35,27 +35,12 @@ MqttItem::~MqttItem() 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() @@ -77,53 +62,89 @@ void MqttItem::refreshSubscription() connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived); } -void MqttItem::onDevicesMessageReceived(const QMqttMessage& message) +bool MqttItem::setFromDevices(const QJsonArray& devices) +{ + QJsonObject device = getSelfFromDevices(devices); + if(device.empty()) + return false; + return loadExposeFromDevice(device); +} + +std::vector MqttItem::getAvailableValueKeys(const QJsonArray& devices) +{ + QJsonObject device = getSelfFromDevices(devices); + if(device.empty()) + return std::vector(); + + QJsonObject definition = device["definition"].toObject(); + if(definition.isEmpty()) + { + qWarning() << "MqttItem" << topic_ << "device has no definition (unsupported)"; + return std::vector(); + } + + QJsonArray exposes = definition["exposes"].toArray(); + if(exposes.isEmpty()) + { + qWarning() << "MqttItem" << topic_ << "device has no exposes"; + return std::vector(); + } + + std::vector values; + for(const QJsonValue& exposeValue : exposes) + { + if(!exposeValue.isObject()) + continue; + + QJsonObject expose = exposeValue.toObject(); + if(expose.contains("property")) + values.push_back(expose["property"].toString()); + + // Check if it's a composite type with features + if(expose.contains("features")) + { + QJsonArray features = expose["features"].toArray(); + for(const QJsonValue& featureValue : features) + { + if(!featureValue.isObject()) + continue; + + QJsonObject feature = featureValue.toObject(); + if(feature["property"].toString() == valueKey_) + values.push_back(feature["property"].toString()); + } + } + } + return values; +} + +QJsonObject MqttItem::getSelfFromDevices(const QJsonArray& devices) { - 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; - } + return device; } + qWarning()<<"Could not find own topic in devices array"; + return QJsonObject(); } -void MqttItem::loadExposeFromDevice(const QJsonObject& device) +bool 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; + return false; } // Get exposes from definition @@ -131,7 +152,7 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device) if(exposes.isEmpty()) { qWarning() << "MqttItem" << topic_ << "device has no exposes"; - return; + return false; } for(const QJsonValue& exposeValue : exposes) @@ -147,11 +168,11 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device) { setFromExpose(expose); qDebug() << "MqttItem" << topic_ << "detected type" << expose["type"].toString() << "for property" << valueKey_; - return; + return false; } // Check if it's a composite type with features - if(expose["type"].toString() == "composite" || expose["type"].toString() == "light") + if(expose.contains("features")) { QJsonArray features = expose["features"].toArray(); for(const QJsonValue& featureValue : features) @@ -164,13 +185,14 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device) { setFromExpose(feature); qDebug() << "MqttItem" << topic_ << "detected type" << feature["type"].toString() << "for property" << valueKey_; - return; + return true; } } } } - qWarning() << "MqttItem" << topic_ << "could not find expose for property" << valueKey_; + qWarning()<<"MqttItem"< 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_; @@ -353,11 +351,6 @@ int MqttItem::getValueStep() const return valueStep_; } -bool MqttItem::getExposeLoaded() const -{ - return exposeLoaded_; -} - void MqttItem::store(QJsonObject& json) { Item::store(json); @@ -381,7 +374,6 @@ void MqttItem::load(const QJsonObject& json, const bool preserve) valueMin_ = json["ValueMin"].toInt(0); valueMax_ = json["ValueMax"].toInt(255); valueStep_ = json["ValueStep"].toInt(1); - exposeLoaded_ = json["ExposeLoaded"].toBool(false); hashId(); refreshSubscription(); } diff --git a/src/items/mqttitem.h b/src/items/mqttitem.h index b0248a2..2b2f423 100644 --- a/src/items/mqttitem.h +++ b/src/items/mqttitem.h @@ -9,8 +9,6 @@ class QString; class MqttItem : public Item { Q_OBJECT -Q_SIGNALS: - void exposeLoaded(); public: inline static std::weak_ptr client; @@ -23,17 +21,17 @@ private: 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); + + bool loadExposeFromDevice(const QJsonObject& device); + void setFromExpose(const QJsonObject& expose); + QJsonObject getSelfFromDevices(const QJsonArray& devices); public: explicit MqttItem(QString name = "MqttItem", @@ -51,11 +49,8 @@ public: void setValueStep(int step); void setValueType(item_value_type_t type); - // Configure from Zigbee2MQTT expose info - void setFromExpose(const QJsonObject& expose); - - // Trigger expose lookup from bridge/devices - void triggerExposeLookup(); + bool setFromDevices(const QJsonArray& devices); + std::vector getAvailableValueKeys(const QJsonArray& devices); QString getTopic() const; QString getValueKey() const; @@ -64,7 +59,6 @@ public: 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; @@ -73,4 +67,4 @@ protected: virtual void enactValue(uint8_t value) override; }; -#endif // MQTTITEM_H \ No newline at end of file +#endif // MQTTITEM_H diff --git a/src/mainobject.cpp b/src/mainobject.cpp index b84a48f..304b47d 100644 --- a/src/mainobject.cpp +++ b/src/mainobject.cpp @@ -8,6 +8,7 @@ #include "mqttclient.h" #include "items/mqttitem.h" #include "items/itemstore.h" +#include "ui/itemsettingswidgets/mqttitemsettingswidget.h" MainObject::MainObject(QObject *parent) : QObject(parent) @@ -176,6 +177,8 @@ SecondaryMainObject::SecondaryMainObject(QString host, int port, QObject *parent QMetaObject::invokeMethod(this, [](){exit(1);}, Qt::QueuedConnection); } + MqttItemSettingsWidget::service = tcpClient; + connect(&globalItems, &ItemStore::itemUpdated, tcpClient, &TcpClient::itemUpdated); globalItems.refresh(); diff --git a/src/microcontroller.cpp b/src/microcontroller.cpp index 2a766b5..df17a45 100644 --- a/src/microcontroller.cpp +++ b/src/microcontroller.cpp @@ -114,7 +114,6 @@ Microcontroller::Microcontroller() writeTimer.setInterval(50); writeTimer.setSingleShot(false); connect(&writeTimer, &QTimer::timeout, this, &Microcontroller::onWriteTimerTimeout); - qDebug()<<__func__< Microcontroller::processRelayLine(const QString& buffer) name.remove(name.size()-1, 1); else name = "Relay " + QString::number(bufferList[1].toInt(nullptr, 2)); - return std::shared_ptr(new Relay(bufferList[2].toInt(), + std::shared_ptr relay(new Relay(bufferList[2].toInt(), name, - bufferList[6].toInt(nullptr, 2), + bufferList[4].toInt(nullptr, 2), bufferList[8].toInt())); + return relay; } return nullptr; } diff --git a/src/mqttclient.cpp b/src/mqttclient.cpp index eb504a6..fae73fc 100644 --- a/src/mqttclient.cpp +++ b/src/mqttclient.cpp @@ -26,6 +26,20 @@ void MqttClient::start(const QJsonObject& settings) client->connectToHost(); } +void MqttClient::onDevicesMessageReceived(const QMqttMessage& message) +{ + QJsonDocument doc = QJsonDocument::fromJson(message.payload()); + if(!doc.isArray()) + return; + qDebug()<<"MqttClient got devices array"; + devices = doc.array(); +} + +QJsonArray MqttClient::getDevicesArray() +{ + return devices; +} + void MqttClient::onClientError(QMqttClient::ClientError error) { qWarning()<<"MQTT Client error:"<subscription, &QMqttSubscription::messageReceived, this, &MqttClient::onDevicesMessageReceived); qInfo()<<"Connected to MQTT broker at "<hostname()<port(); + } else if (state == QMqttClient::ClientState::Disconnected) + { qWarning()<<"Lost connection to MQTT broker"; + } else if(state == QMqttClient::ClientState::Connecting) + { qInfo()<<"Connecting to MQTT broker at "<hostname()<port(); + } } void MqttClient::store(QJsonObject& json) @@ -107,9 +129,11 @@ QString MqttClient::getBaseTopic() MqttClient::~MqttClient() { + if(devicesSubscription) + unsubscribe(devicesSubscription); for(const std::pair sub : subscriptions) { qWarning()<unsubscribe(sub.second->subscription->topic()); } -} \ No newline at end of file +} diff --git a/src/mqttclient.h b/src/mqttclient.h index 1199855..cc99a89 100644 --- a/src/mqttclient.h +++ b/src/mqttclient.h @@ -4,6 +4,7 @@ #include #include #include +#include #include class MqttClient: public QObject @@ -20,10 +21,13 @@ private: QString baseTopicName; std::shared_ptr client; std::map subscriptions; + Subscription* devicesSubscription = nullptr; + QJsonArray devices; private slots: void onClientStateChanged(QMqttClient::ClientState state); void onClientError(QMqttClient::ClientError error); + void onDevicesMessageReceived(const QMqttMessage& message); public: explicit MqttClient(); @@ -35,6 +39,7 @@ public: void unsubscribe(Subscription* subscription); void unsubscribe(QString topic); QString getBaseTopic(); + QJsonArray getDevicesArray(); }; #endif // MQTTCLIENT_H diff --git a/src/sensors/sensor.cpp b/src/sensors/sensor.cpp index 040b02a..a9da16b 100644 --- a/src/sensors/sensor.cpp +++ b/src/sensors/sensor.cpp @@ -5,6 +5,32 @@ SensorStore globalSensors; +QString sensorUpdateTypeToString(sensor_update_type_t type) +{ + switch(type) + { + case SENSOR_UPDATE_USER: + return "User"; + case SENSOR_UPDATE_REMOTE: + return "Remote"; + case SENSOR_UPDATE_BACKEND: + return "Backend"; + default: + return "Invalid"; + } +} + +sensor_update_type_t sensorUpdateTypeFromString(const QString& string) +{ + if(string == sensorUpdateTypeToString(SENSOR_UPDATE_USER)) + return SENSOR_UPDATE_USER; + if(string == sensorUpdateTypeToString(SENSOR_UPDATE_REMOTE)) + return SENSOR_UPDATE_REMOTE; + if(string == sensorUpdateTypeToString(SENSOR_UPDATE_BACKEND)) + return SENSOR_UPDATE_BACKEND; + return SENSOR_UPDATE_INVALID; +} + SensorStore::SensorStore(QObject *parent): QObject(parent) { } @@ -12,7 +38,13 @@ SensorStore::SensorStore(QObject *parent): QObject(parent) void SensorStore::store(QJsonObject& json) { QJsonArray sensorsArray; - for(const Sensor& sensor : sensors_) + std::vector sensors = sensors_; + for(const Sensor& sensor : knownSensors_) + { + if(std::find(sensors.begin(), sensors.end(), sensor) == sensors.end()) + sensors.push_back(sensor); + } + for(const Sensor& sensor : sensors) { QJsonObject sensorObject; sensor.store(sensorObject); diff --git a/src/sensors/sensor.h b/src/sensors/sensor.h index 012cb4b..86d79db 100644 --- a/src/sensors/sensor.h +++ b/src/sensors/sensor.h @@ -176,6 +176,9 @@ typedef enum { SENSOR_UPDATE_INVALID } sensor_update_type_t; +QString sensorUpdateTypeToString(sensor_update_type_t type); +sensor_update_type_t sensorUpdateTypeFromString(const QString& string); + class SensorStore: public QObject { Q_OBJECT diff --git a/src/service/server.cpp b/src/service/server.cpp index b979413..6c4a087 100644 --- a/src/service/server.cpp +++ b/src/service/server.cpp @@ -3,6 +3,7 @@ #include #include "items/item.h" +#include "items/mqttitem.h" #include "service.h" #include "server.h" @@ -33,7 +34,6 @@ void Server::processIncomeingJson(const QByteArray& jsonbytes) if(item) { qDebug()<<"Server got item"<getName(); - item->setLoaded(FullList); fieldChanges.push_back(item->loadWithChanges(jsonobject)); items.push_back(item); } @@ -57,12 +57,24 @@ void Server::processIncomeingJson(const QByteArray& jsonbytes) updateItems(updates); } } + else if(type == "GetMqttDevices") + { + sendMqttDevices(); + } else { Service::processIncomeingJson(jsonbytes); } } +void Server::sendMqttDevices() +{ + std::weak_ptr client = MqttItem::client; + std::shared_ptr workClient = client.lock(); + if(workClient) + sendJson(createMessage("MqttDevices", workClient->getDevicesArray())); +} + void Server::handleSocketError() { QObject* obj = sender(); @@ -112,4 +124,4 @@ void Server::itemUpdated(ItemUpdateRequest update) QJsonObject json = createMessage("ItemUpdate", items); json["FullList"] = false; sendJson(json); -} \ No newline at end of file +} diff --git a/src/service/server.h b/src/service/server.h index 5564108..5e09f46 100644 --- a/src/service/server.h +++ b/src/service/server.h @@ -44,6 +44,7 @@ protected: void handleSocketDisconnect(); void removeClient(QTcpSocket* socket); void removeClient(QWebSocket* socket); + void sendMqttDevices(); signals: void sigRequestSave(); diff --git a/src/service/service.cpp b/src/service/service.cpp index 6117008..3e86b43 100644 --- a/src/service/service.cpp +++ b/src/service/service.cpp @@ -48,6 +48,12 @@ void Service::refresh() sendJson(createMessage("GetItems", QJsonArray())); } +void Service::requestMqttDevices() +{ + qDebug()<<__func__; + sendJson(createMessage("GetMqttDevices", QJsonArray())); +} + void Service::sendSensors() { QJsonArray sensors; @@ -96,7 +102,17 @@ void Service::processIncomeingJson(const QByteArray& jsonbytes) { QJsonObject jsonobject = sensorjson.toObject(); Sensor sensor(jsonobject); - gotSensor(sensor, SENSOR_UPDATE_REMOTE); + sensor_update_type_t updateType = SENSOR_UPDATE_REMOTE; + if(jsonobject.contains("UpdateType")) + { + updateType = sensorUpdateTypeFromString(jsonobject["UpdateType"].toString()); + if(updateType == SENSOR_UPDATE_INVALID) + { + qWarning()<<"Got invalid sensor update type"< item = Item::loadItem(jsonobject); if(item) { - item->setLoaded(FullList); fieldChanges.push_back(item->loadWithChanges(jsonobject)); items.push_back(item); } diff --git a/src/ui/itemsettingswidgets/mqttitemsettingswidget.cpp b/src/ui/itemsettingswidgets/mqttitemsettingswidget.cpp index f566ad9..72b1440 100644 --- a/src/ui/itemsettingswidgets/mqttitemsettingswidget.cpp +++ b/src/ui/itemsettingswidgets/mqttitemsettingswidget.cpp @@ -1,7 +1,9 @@ #include "mqttitemsettingswidget.h" #include "ui_mqttitemsettingswidget.h" +#include "programmode.h" #include +#include #include MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr item, QWidget *parent) : @@ -39,43 +41,6 @@ MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr item, QWi updateValueNamesFromItem(); suppressUpdates_ = false; updateVisibility(); - - // Connect expose loaded signal - connect(workingItem.get(), &MqttItem::exposeLoaded, this, [this]() { - if(auto item = item_.lock()) - { - suppressUpdates_ = true; - ui->label_status->setText("Detected!"); - - // Update value type - switch(item->getValueType()) - { - case ITEM_VALUE_UINT: - ui->comboBox_valueType->setCurrentIndex(1); - break; - case ITEM_VALUE_ENUM: - ui->comboBox_valueType->setCurrentIndex(2); - break; - default: - ui->comboBox_valueType->setCurrentIndex(0); - break; - } - - // Update limits - ui->spinBox_min->setValue(item->getValueMin()); - ui->spinBox_max->setValue(item->getValueMax()); - ui->spinBox_step->setValue(item->getValueStep()); - - // Update value on/off - ui->lineEdit_valueOn->setText(item->getValueOn()); - ui->lineEdit_valueOff->setText(item->getValueOff()); - - // Update value names - updateValueNamesFromItem(); - suppressUpdates_ = false; - updateVisibility(); - } - }); } // Connect signals @@ -91,6 +56,60 @@ MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr item, QWi connect(ui->pushButton_addValueName, &QPushButton::clicked, this, &MqttItemSettingsWidget::onAddValueName); connect(ui->pushButton_removeValueName, &QPushButton::clicked, this, &MqttItemSettingsWidget::onRemoveValueName); connect(ui->listWidget_valueNames, &QListWidget::itemChanged, this, &MqttItemSettingsWidget::onValueNamesChanged); + + if(service) + connect(service, &Service::gotMqttDevices, this, &MqttItemSettingsWidget::onGotMqttDevices); + else if(programMode == PROGRAM_MODE_UI_ONLY) + qWarning()<<__func__<<"with no service available"; +} + +void MqttItemSettingsWidget::onGotMqttDevices(QJsonArray devices) +{ + if(auto item = item_.lock()) + { + bool ret = item->setFromDevices(devices); + if(!ret) + QMessageBox::warning(this, "Unable to get expose", "Unable to load expose from devices"); + else + exposeLoaded(); + } +} + +void MqttItemSettingsWidget::exposeLoaded() +{ + if(auto item = item_.lock()) + { + suppressUpdates_ = true; + ui->label_status->setText("Detected!"); + + // Update value type + switch(item->getValueType()) + { + case ITEM_VALUE_UINT: + ui->comboBox_valueType->setCurrentIndex(1); + break; + case ITEM_VALUE_ENUM: + ui->comboBox_valueType->setCurrentIndex(2); + break; + default: + ui->comboBox_valueType->setCurrentIndex(0); + break; + } + + // Update limits + ui->spinBox_min->setValue(item->getValueMin()); + ui->spinBox_max->setValue(item->getValueMax()); + ui->spinBox_step->setValue(item->getValueStep()); + + // Update value on/off + ui->lineEdit_valueOn->setText(item->getValueOn()); + ui->lineEdit_valueOff->setText(item->getValueOff()); + + // Update value names + updateValueNamesFromItem(); + suppressUpdates_ = false; + updateVisibility(); + } } void MqttItemSettingsWidget::setTopic(const QString& topic) @@ -186,7 +205,24 @@ void MqttItemSettingsWidget::onAutoDetectClicked() if(auto workingItem = item_.lock()) { ui->label_status->setText("Detecting..."); - workingItem->triggerExposeLookup(); + std::shared_ptr workClient = MqttItem::client.lock(); + if(workClient) + { + bool ret = workingItem->setFromDevices(workClient->getDevicesArray()); + if(!ret) + { + QMessageBox::warning(this, "Unable to get expose", "Unable to load expose from devices"); + qDebug()<<"Has exposes:"<getAvailableValueKeys(workClient->getDevicesArray()); + } + else + { + exposeLoaded(); + } + } + else if(service) + { + service->requestMqttDevices(); + } } } @@ -269,4 +305,4 @@ void MqttItemSettingsWidget::updateValueNamesFromItem() MqttItemSettingsWidget::~MqttItemSettingsWidget() { delete ui; -} \ No newline at end of file +} diff --git a/src/ui/itemsettingswidgets/mqttitemsettingswidget.h b/src/ui/itemsettingswidgets/mqttitemsettingswidget.h index 4ad97ac..f365d03 100644 --- a/src/ui/itemsettingswidgets/mqttitemsettingswidget.h +++ b/src/ui/itemsettingswidgets/mqttitemsettingswidget.h @@ -4,6 +4,7 @@ #include #include #include "../../items/mqttitem.h" +#include "../../service/service.h" namespace Ui { @@ -13,9 +14,17 @@ class MqttItemSettingsWidget; class MqttItemSettingsWidget : public QWidget { Q_OBJECT + +public: + inline static Service* service = nullptr; + +private: + std::weak_ptr item_; bool suppressUpdates_ = false; + void exposeLoaded(); + private slots: void setTopic(const QString& topic); void setValueKey(const QString& valueKey); @@ -29,6 +38,7 @@ private slots: void onAddValueName(); void onRemoveValueName(); void onValueNamesChanged(); + void onGotMqttDevices(QJsonArray devices); public: explicit MqttItemSettingsWidget(std::weak_ptr item, QWidget *parent = nullptr); @@ -41,4 +51,4 @@ private: void syncValueNamesToItem(); }; -#endif // MQTTITEMSETTINGSWIDGET_H \ No newline at end of file +#endif // MQTTITEMSETTINGSWIDGET_H diff --git a/src/ui/itemwidget.cpp b/src/ui/itemwidget.cpp index 336ffb2..614e71f 100644 --- a/src/ui/itemwidget.cpp +++ b/src/ui/itemwidget.cpp @@ -15,6 +15,8 @@ ItemWidget::ItemWidget(std::weak_ptr item, bool noGroupEdit, QWidget *pare { ui->setupUi(this); + connect(&valueTimer, &QTimer::timeout, this, &ItemWidget::onValueTimerTimeout); + if(auto workingItem = item_.lock()) { if(workingItem->getValueType() == ITEM_VALUE_UINT) @@ -76,6 +78,21 @@ void ItemWidget::deleteItem() } void ItemWidget::moveToValue(int value) +{ + valueQue.enqueue(value); + if(!valueTimer.isActive()) + { + valueTimer.setInterval(0); + valueTimer.start(); + } +} + +void ItemWidget::moveToState(bool state) +{ + moveToValue(state); +} + +void ItemWidget::applyValue(int value) { if(auto workingItem = item_.lock()) { @@ -90,9 +107,19 @@ void ItemWidget::moveToValue(int value) } } -void ItemWidget::moveToState(bool state) +void ItemWidget::onValueTimerTimeout() { - moveToValue(state); + valueTimer.setInterval(50); + if(!valueQue.empty()) + { + while(valueQue.count() > 2) + valueQue.dequeue(); + applyValue(valueQue.dequeue()); + } + else + { + valueTimer.stop(); + } } void ItemWidget::disable() diff --git a/src/ui/itemwidget.h b/src/ui/itemwidget.h index 51c642f..accca1f 100644 --- a/src/ui/itemwidget.h +++ b/src/ui/itemwidget.h @@ -2,6 +2,8 @@ #define RELAYWIDGET_H #include +#include +#include #include #include "../items/item.h" @@ -17,6 +19,9 @@ private: std::weak_ptr item_; bool noGroupEdit_; + QQueue valueQue; + QTimer valueTimer; + void disable(); signals: @@ -28,6 +33,7 @@ private slots: void moveToState(bool state); void moveToValue(int value); void deleteItem(); + void onValueTimerTimeout(); public: explicit ItemWidget(std::weak_ptr item, bool noGroupEdit = false, QWidget *parent = nullptr); @@ -41,6 +47,7 @@ public slots: void onItemUpdated(ItemUpdateRequest update); private: + void applyValue(int value); Ui::ItemWidget *ui; }; diff --git a/tests/unit/items/test_item.cpp b/tests/unit/items/test_item.cpp index b4ad369..5c4a32d 100644 --- a/tests/unit/items/test_item.cpp +++ b/tests/unit/items/test_item.cpp @@ -73,15 +73,6 @@ private slots: QVERIFY(!item.isHidden()); } - void testItemLoaded() - { - Item item(1, "test_item", 0); - QVERIFY(!item.getLoaded()); - - item.setLoaded(true); - QVERIFY(item.getLoaded()); - } - void testItemJsonSerialization() { Item item(42, "test_item", 1); diff --git a/tests/unit/items/test_mqttitem.cpp b/tests/unit/items/test_mqttitem.cpp index 801a63e..1989732 100644 --- a/tests/unit/items/test_mqttitem.cpp +++ b/tests/unit/items/test_mqttitem.cpp @@ -138,66 +138,6 @@ private slots: QVERIFY(item.indexToValueName(99).isEmpty()); // Out of bounds } - void testSetFromExposeBinary() - { - MqttItem item("test_mqtt", 0); - - QJsonObject expose; - expose["type"] = "binary"; - expose["property"] = "state"; - expose["value_on"] = "ON"; - expose["value_off"] = "OFF"; - - item.setFromExpose(expose); - - QVERIFY(item.getValueType() == ITEM_VALUE_BOOL); - QVERIFY(item.getValueKey() == "state"); - QVERIFY(item.getValueOn() == "ON"); - QVERIFY(item.getValueOff() == "OFF"); - } - - void testSetFromExposeNumeric() - { - MqttItem item("test_mqtt", 0); - - QJsonObject expose; - expose["type"] = "numeric"; - expose["property"] = "brightness"; - expose["value_min"] = 0; - expose["value_max"] = 254; - expose["value_step"] = 1; - - item.setFromExpose(expose); - - QVERIFY(item.getValueType() == ITEM_VALUE_UINT); - QVERIFY(item.getValueKey() == "brightness"); - QVERIFY(item.getValueMin() == 0); - QVERIFY(item.getValueMax() == 254); - QVERIFY(item.getValueStep() == 1); - } - - void testSetFromExposeEnum() - { - MqttItem item("test_mqtt", 0); - - QJsonObject expose; - expose["type"] = "enum"; - expose["property"] = "system_mode"; - expose["values"] = QJsonArray{"off", "heat", "cool", "auto"}; - - item.setFromExpose(expose); - - QVERIFY(item.getValueType() == ITEM_VALUE_ENUM); - QVERIFY(item.getValueKey() == "system_mode"); - - auto names = item.getValueNames(); - QVERIFY(names.size() == 4); - QVERIFY(names[0] == "off"); - QVERIFY(names[1] == "heat"); - QVERIFY(names[2] == "cool"); - QVERIFY(names[3] == "auto"); - } - void testJsonSerialization() { MqttItem item("test_mqtt", 1); @@ -237,186 +177,6 @@ private slots: QVERIFY(item.getValueOff() == "OFF"); } - void testLoadExposeFromDevice() - { - // Create item with specific topic and valueKey - MqttItem item("test", 0); - item.setTopic("0xa4c138ef510950e3"); - item.setValueKey("system_mode"); - - // Simulate device data from zigbee2mqtt/bridge/devices - QJsonObject device; - device["friendly_name"] = "0xa4c138ef510950e3"; - device["ieee_address"] = "0xa4c138ef510950e3"; - - QJsonObject definition; - definition["model"] = "TS0601_thermostat"; - definition["vendor"] = "Tuya"; - definition["description"] = "Thermostat"; - - QJsonArray exposes; - - // Binary expose - QJsonObject stateExpose; - stateExpose["type"] = "binary"; - stateExpose["property"] = "state"; - stateExpose["value_on"] = "ON"; - stateExpose["value_off"] = "OFF"; - exposes.append(stateExpose); - - // Enum expose - the one we're looking for - QJsonObject systemModeExpose; - systemModeExpose["type"] = "enum"; - systemModeExpose["property"] = "system_mode"; - systemModeExpose["values"] = QJsonArray{"off", "heat", "cool", "auto"}; - exposes.append(systemModeExpose); - - // Numeric expose - QJsonObject tempExpose; - tempExpose["type"] = "numeric"; - tempExpose["property"] = "current_temperature"; - tempExpose["value_min"] = 0; - tempExpose["value_max"] = 100; - exposes.append(tempExpose); - - definition["exposes"] = exposes; - device["definition"] = definition; - - // Call the private method via public API - we need to test the logic - // Since loadExposeFromDevice is private, we test via setFromExpose - QJsonObject enumExpose; - enumExpose["type"] = "enum"; - enumExpose["property"] = "system_mode"; - enumExpose["values"] = QJsonArray{"off", "heat", "cool", "auto"}; - - item.setFromExpose(enumExpose); - - QVERIFY(item.getValueType() == ITEM_VALUE_ENUM); - QVERIFY(item.getValueNames().size() == 4); - } - - // Note: Full integration tests for onDevicesMessageReceived require QMqttMessage construction - // which is not possible without making it a friend. The setFromExpose tests below verify - // the core valueType determination logic that onDevicesMessageReceived uses internally. - // The full flow (device matching + expose parsing) is tested via setFromExpose. - - void testValueTypeDeterminationEnumViaExpose() - { - // Test enum valueType determination - simulates what loadExposeFromDevice extracts - MqttItem item("test", 0); - item.setTopic("0xa4c138ef510950e3"); - item.setValueKey("system_mode"); - - // Simulate the expose object that would be found in bridge/devices - QJsonObject expose; - expose["type"] = "enum"; - expose["property"] = "system_mode"; - expose["values"] = QJsonArray{"off", "heat", "auto"}; - - item.setFromExpose(expose); - - QVERIFY2(item.getValueType() == ITEM_VALUE_ENUM, "ValueType should be ENUM"); - QVERIFY2(item.getValueKey() == "system_mode", "ValueKey should be set"); - - auto names = item.getValueNames(); - QVERIFY2(names.size() == 3, "Should have 3 enum values"); - QVERIFY2(names[0] == "off", "First value should be 'off'"); - QVERIFY2(names[1] == "heat", "Second value should be 'heat'"); - QVERIFY2(names[2] == "auto", "Third value should be 'auto'"); - } - - void testValueTypeDeterminationNumericViaExpose() - { - // Test numeric valueType determination - MqttItem item("test", 0); - item.setTopic("0xa4c138d9a039b6df"); - item.setValueKey("temperature"); - - QJsonObject expose; - expose["type"] = "numeric"; - expose["property"] = "temperature"; - expose["value_min"] = -40; - expose["value_max"] = 80; - expose["value_step"] = 0.1; // Note: toInt() on double returns default, so step becomes 1 - - item.setFromExpose(expose); - - QVERIFY2(item.getValueType() == ITEM_VALUE_UINT, "ValueType should be UINT"); - QVERIFY2(item.getValueMin() == -40, "Min should be -40"); - QVERIFY2(item.getValueMax() == 80, "Max should be 80"); - QVERIFY2(item.getValueStep() == 1, "Step should be 1 (toInt on double returns default)"); - } - - void testValueTypeDeterminationBinaryViaExpose() - { - // Test binary valueType determination - MqttItem item("test", 0); - item.setTopic("0xa4c138f3d3cf8700"); - item.setValueKey("presence"); - - QJsonObject expose; - expose["type"] = "binary"; - expose["property"] = "presence"; - expose["value_on"] = "ON"; // Use string values for proper conversion - expose["value_off"] = "OFF"; - - item.setFromExpose(expose); - - QVERIFY2(item.getValueType() == ITEM_VALUE_BOOL, "ValueType should be BOOL"); - QVERIFY2(item.getValueOn() == "ON", "ValueOn should be 'ON'"); - QVERIFY2(item.getValueOff() == "OFF", "ValueOff should be 'OFF'"); - } - - void testValueTypeDeterminationCompositeFeatureViaExpose() - { - // Test composite/climate feature valueType determination - MqttItem item("test", 0); - item.setTopic("0xa4c138ef510950e3"); - item.setValueKey("current_heating_setpoint"); - - // Simulate a feature from a composite/climate type - QJsonObject feature; - feature["type"] = "numeric"; - feature["property"] = "current_heating_setpoint"; - feature["value_min"] = 5; - feature["value_max"] = 35; - feature["value_step"] = 0.5; - - item.setFromExpose(feature); - - QVERIFY2(item.getValueType() == ITEM_VALUE_UINT, "ValueType should be UINT for numeric feature"); - QVERIFY2(item.getValueMin() == 5, "Min should be 5"); - QVERIFY2(item.getValueMax() == 35, "Max should be 35"); - } - - void testRealDeviceExposeFromMqttBroker() - { - // Integration test: Verify valueType determination works with real device data - // from the MQTT broker. This tests the actual zigbee2mqtt bridge/devices format. - - // Create item matching a real device on the broker - MqttItem item("test", 0); - item.setTopic("0xa4c138ef510950e3"); - item.setValueKey("system_mode"); - - // The real device has system_mode as an enum with values ["auto", "heat", "off"] - // This matches the actual expose from zigbee2mqtt/bridge/devices - QJsonObject expose; - expose["type"] = "enum"; - expose["property"] = "system_mode"; - expose["values"] = QJsonArray{"auto", "heat", "off"}; - - item.setFromExpose(expose); - - QVERIFY2(item.getValueType() == ITEM_VALUE_ENUM, "Real device: ValueType should be ENUM"); - - auto names = item.getValueNames(); - QVERIFY2(names.size() == 3, "Real device: Should have 3 enum values"); - QVERIFY2(names[0] == "auto", "Real device: First value should be 'auto'"); - QVERIFY2(names[1] == "heat", "Real device: Second value should be 'heat'"); - QVERIFY2(names[2] == "off", "Real device: Third value should be 'off'"); - } - void cleanupTestCase() { // Cleanup after all tests @@ -425,4 +185,4 @@ private slots: QTEST_APPLESS_MAIN(TestMqttItem) -#include "test_mqttitem.moc" \ No newline at end of file +#include "test_mqttitem.moc"