diff --git a/CMakeLists.txt b/CMakeLists.txt index 6508688..ca60d65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 4.0) -project(smartvos VERSION 1.0 LANGUAGES CXX) +project(SHinterface VERSION 1.0 LANGUAGES CXX) # Set C++ standard set(CMAKE_CXX_STANDARD 20) @@ -27,113 +27,88 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) # Enable testing framework enable_testing() -# Define shared sources for static library (core sources used by both main and tests) -set(SHINTERFACE_CORE_SOURCES - src/sensors/mqttsensorsource.h - src/sensors/mqttsensorsource.cpp - src/items/mqttitem.h - src/items/mqttitem.cpp - src/mqttclient.h - src/mqttclient.cpp - src/microcontroller.h - src/microcontroller.cpp - src/sun.h - src/sun.cpp - src/programmode.h - src/programmode.cpp - - src/service/service.h - src/service/service.cpp - src/service/tcpclient.h - src/service/tcpclient.cpp - src/service/server.h - src/service/server.cpp - src/service/tcpserver.h - src/service/tcpserver.cpp - src/service/websocketserver.h - src/service/websocketserver.cpp - - src/actors/actor.h - src/actors/actor.cpp - src/actors/factoractor.h - src/actors/factoractor.cpp - src/actors/polynomalactor.h - src/actors/polynomalactor.cpp - src/actors/sensoractor.h - src/actors/sensoractor.cpp - src/actors/alarmtime.h - src/actors/alarmtime.cpp - src/actors/regulator.h - src/actors/regulator.cpp - src/actors/timeractor.h - src/actors/timeractor.cpp - - src/sensors/sensor.h - src/sensors/sensor.cpp - src/sensors/sunsensor.h - src/sensors/sunsensor.cpp - - src/items/item.h - src/items/item.cpp - src/items/relay.h - src/items/relay.cpp - src/items/poweritem.h - src/items/poweritem.cpp - src/items/messageitem.h - src/items/messageitem.cpp - src/items/systemitem.h - src/items/systemitem.cpp - src/items/auxitem.h - src/items/auxitem.cpp - src/items/rgbitem.h - src/items/rgbitem.cpp - src/items/itemsource.h - src/items/itemsource.cpp - src/items/itemloadersource.h - src/items/itemloadersource.cpp - src/items/fixeditemsource.h - src/items/fixeditemsource.cpp - src/items/itemstore.h - src/items/itemstore.cpp -) - -# Create static library -add_library(smartvos_core STATIC ${SHINTERFACE_CORE_SOURCES}) - -# Link Qt and system libraries to static library -target_link_libraries(smartvos_core - Qt6::Core - Qt6::Gui - Qt6::Widgets - Qt6::Network - Qt6::Multimedia - Qt6::SerialPort - Qt6::Mqtt - Qt6::WebSockets - ${PIPEWIRE_LIBRARIES} - ${LIBNL3_LIBRARIES} -) - -# Add include paths to static library -target_include_directories(smartvos_core PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src - ${PIPEWIRE_INCLUDE_DIRS} - ${LIBNL3_INCLUDE_DIRS} -) - # Add subdirectory for tests add_subdirectory(tests) # Create executable -add_executable(smartvos +add_executable(SHinterface + src/sensors/mqttsensorsource.h src/sensors/mqttsensorsource.cpp + src/items/mqttitem.h src/items/mqttitem.cpp + src/mqttclient.h src/mqttclient.cpp + +) + +# Add sources to executable +target_sources(SHinterface + PRIVATE src/main.cpp src/mainobject.h src/mainobject.cpp src/apgetconnected.h src/apgetconnected.cpp + src/microcontroller.h + src/microcontroller.cpp + src/sun.h + src/sun.cpp + src/programmode.h + src/programmode.cpp src/pipewire.h src/pipewire.cpp + src/service/service.h + src/service/service.cpp + src/service/tcpclient.h + src/service/tcpclient.cpp + src/service/server.h + src/service/server.cpp + src/service/tcpserver.h + src/service/tcpserver.cpp + src/service/websocketserver.h + src/service/websocketserver.cpp + + src/actors/actor.h + src/actors/actor.cpp + src/actors/factoractor.h + src/actors/factoractor.cpp + src/actors/polynomalactor.h + src/actors/polynomalactor.cpp + src/actors/sensoractor.h + src/actors/sensoractor.cpp + src/actors/alarmtime.h + src/actors/alarmtime.cpp + src/actors/regulator.h + src/actors/regulator.cpp + src/actors/timeractor.h + src/actors/timeractor.cpp + + src/sensors/sensor.h + src/sensors/sensor.cpp + src/sensors/sunsensor.h + src/sensors/sunsensor.cpp + + src/items/item.h + src/items/item.cpp + src/items/relay.h + src/items/relay.cpp + src/items/poweritem.h + src/items/poweritem.cpp + src/items/messageitem.h + src/items/messageitem.cpp + src/items/systemitem.h + src/items/systemitem.cpp + src/items/auxitem.h + src/items/auxitem.cpp + src/items/rgbitem.h + src/items/rgbitem.cpp + src/items/itemsource.h + src/items/itemsource.cpp + src/items/itemloadersource.h + src/items/itemloadersource.cpp + src/items/fixeditemsource.h + src/items/fixeditemsource.cpp + src/items/itemstore.h + src/items/itemstore.cpp + src/ui/mainwindow.h src/ui/mainwindow.cpp src/ui/itemwidget.h @@ -173,7 +148,7 @@ add_executable(smartvos ) # Add UI files -target_sources(smartvos +target_sources(SHinterface PRIVATE src/ui/mainwindow.ui src/ui/itemwidget.ui @@ -194,14 +169,13 @@ target_sources(smartvos ) # Add resource file -target_sources(smartvos +target_sources(SHinterface PRIVATE resources.qrc ) -# Link libraries - link to static library plus UI-specific dependencies -target_link_libraries(smartvos - smartvos_core +# Link libraries +target_link_libraries(SHinterface Qt6::Core Qt6::Gui Qt6::Widgets @@ -216,16 +190,3 @@ target_link_libraries(smartvos # Add include paths include_directories(${PIPEWIRE_INCLUDE_DIRS} ${LIBNL3_INCLUDE_DIRS}) - -# Installation -install(TARGETS smartvos DESTINATION bin) -install(TARGETS smartvos_core DESTINATION lib) - -# Install icon -install(FILES UVOSicon.bmp DESTINATION share/icons/hicolor/48x48/apps RENAME smartvos.png) - -# Install .desktop file -install(FILES smartvos.desktop DESTINATION share/applications) - -# Update icon cache (optional, for icon themes) -install(CODE "execute_process(COMMAND gtk-update-icon-cache -f -t ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX})") diff --git a/smartvos.desktop b/smartvos.desktop deleted file mode 100644 index adc785e..0000000 --- a/smartvos.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Name=SmartVOS -Comment=Smart Home Interface -Exec=smartvos -Icon=smartvos -Terminal=false -Type=Application -Categories=Utility;HomeAutomation; -Keywords=smart;home;automation;iot; -StartupNotify=true \ No newline at end of file diff --git a/src/items/item.cpp b/src/items/item.cpp index 14d31b3..b902c32 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -60,13 +60,6 @@ void ItemData::storeWithChanges(QJsonObject& json, const ItemFieldChanges& chang json["Value"] = static_cast(value_); if(changes.groupName) json["GroupName"] = groupName_; - if(changes.valueNames) - { - QJsonArray valueNamesArray; - for(const QString& name : valueNames_) - valueNamesArray.append(name); - json["ValueNames"] = valueNamesArray; - } } void ItemData::load(const QJsonObject &json, const bool preserve) @@ -94,19 +87,6 @@ ItemFieldChanges ItemData::loadWithChanges(const QJsonObject& json, const bool p groupName_ = json["GroupName"].toString(); changes.groupName = true; } - if(json.contains("ValueType")) - { - type_ = static_cast(json["ValueType"].toInt()); - changes.type = true; - } - if(json.contains("ValueNames")) - { - valueNames_.clear(); - QJsonArray valueNamesArray = json["ValueNames"].toArray(); - for(int i = 0; i < valueNamesArray.size(); ++i) - valueNames_.push_back(valueNamesArray[i].toString()); - changes.valueNames = true; - } itemId_ = static_cast(json["ItemId"].toDouble(0)); } return changes; @@ -140,8 +120,6 @@ bool ItemData::hasChanged(const ItemData& other, const ItemFieldChanges& changes return true; if(changes.actors) return true; - if(changes.valueNames && other.getValueNames() != getValueNames()) - return true; return false; } @@ -170,33 +148,6 @@ void ItemData::setGroupName(QString groupName) groupName_ = groupName; } -std::vector ItemData::getValueNames() const -{ - return valueNames_; -} - -void ItemData::setValueNames(std::vector valueNames) -{ - valueNames_ = std::move(valueNames); -} - -int ItemData::valueNameToIndex(const QString& name) const -{ - for(size_t i = 0; i < valueNames_.size(); ++i) - { - if(valueNames_[i] == name) - return static_cast(i); - } - return -1; -} - -QString ItemData::indexToValueName(int index) const -{ - if(index >= 0 && static_cast(index) < valueNames_.size()) - return valueNames_[index]; - return QString(); -} - //item Item::Item(uint32_t itemIdIn, QString name, uint8_t value, QObject *parent): QObject(parent), ItemData (itemIdIn, name, @@ -254,7 +205,6 @@ Item& Item::operator=(const ItemData& other) itemId_ = other.id(); hidden_ = other.isHidden(); groupName_ = other.getGroupName(); - valueNames_ = other.getValueNames(); return *this; } @@ -298,8 +248,6 @@ void Item::requestUpdate(ItemUpdateRequest update) for(std::shared_ptr& actor : update.newActors) addActor(actor); } - if(update.changes.valueNames) - valueNames_ = update.payload.getValueNames(); update.payload = *this; updated(update); } diff --git a/src/items/item.h b/src/items/item.h index 4a70fae..b12575c 100644 --- a/src/items/item.h +++ b/src/items/item.h @@ -71,10 +71,6 @@ public: item_value_type_t getValueType(); QString getGroupName() const; void setGroupName(QString groupName); - std::vector getValueNames() const; - void setValueNames(std::vector valueNames); - int valueNameToIndex(const QString& name) const; - QString indexToValueName(int index) const; void storeWithChanges(QJsonObject& json, const ItemFieldChanges& changes); ItemFieldChanges loadWithChanges(const QJsonObject& json, const bool preserve = false); virtual QString getName() const; @@ -134,7 +130,6 @@ struct ItemFieldChanges bool type :1; bool groupName :1; bool actors :1; - bool valueNames :1; ItemFieldChanges(bool defaultVal = false) { name = defaultVal; @@ -143,11 +138,10 @@ struct ItemFieldChanges type = defaultVal; groupName = defaultVal; actors = false; - valueNames = defaultVal; } inline bool isNone() const { - return !name && !value && !hidden && !type && !groupName && !actors && !valueNames; + return !name && !value && !hidden && !type && !groupName && !actors; } }; diff --git a/src/items/itemloadersource.cpp b/src/items/itemloadersource.cpp index e2d1e68..db5478a 100644 --- a/src/items/itemloadersource.cpp +++ b/src/items/itemloadersource.cpp @@ -26,7 +26,6 @@ void ItemLoaderSource::refresh() request.type = ITEM_UPDATE_LOADED; request.payload = newItem; request.changes = ItemFieldChanges(true); - request.changes.value = false; itemAddRequests.push_back(request); } } diff --git a/src/items/mqttitem.cpp b/src/items/mqttitem.cpp index 9c2ea78..a6b30e5 100644 --- a/src/items/mqttitem.cpp +++ b/src/items/mqttitem.cpp @@ -1,12 +1,10 @@ #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), @@ -17,45 +15,25 @@ MqttItem::MqttItem(QString name, uint8_t value, QObject *parent) { hashId(); std::shared_ptr workClient = client.lock(); - assert(workClient || programMode == PROGRAM_MODE_UI_ONLY); + assert(workClient); - if(workClient) - connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged); + connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged); } MqttItem::~MqttItem() { qDebug()<<__func__; std::shared_ptr workClient = client.lock(); - if(!workClient) + if(!workClient || topic_.isEmpty() || !subscription) 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); - } + workClient->unsubscribe(subscription); } 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,101 +55,6 @@ 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()); @@ -180,32 +63,13 @@ void MqttItem::onMessageReceived(const QMqttMessage& message) QJsonObject obj = doc.object(); if(obj.contains(getValueKey())) { - QJsonValue value = obj[getValueKey()]; + QString value = obj[getValueKey()].toString(); 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); - } + if(value == getValueOn()) + req.payload.setValueData(true); else - { - // Binary value - QString strValue = value.toString(); - if(strValue == getValueOn() || strValue == "ON" || strValue == "true") - req.payload.setValueData(true); - else - req.payload.setValueData(false); - } + req.payload.setValueData(false); requestUpdate(req); } } @@ -240,59 +104,6 @@ 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_; @@ -313,26 +124,6 @@ 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); @@ -341,9 +132,6 @@ 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) @@ -353,10 +141,6 @@ 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(); } @@ -370,18 +154,7 @@ void MqttItem::enactValue(uint8_t value) 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_; - } + 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 f31203c..c092444 100644 --- a/src/items/mqttitem.h +++ b/src/items/mqttitem.h @@ -15,22 +15,15 @@ public: private: QString topic_; QString valueKey_; - QString valueOn_ = "ON"; - QString valueOff_ = "OFF"; - int valueMin_ = 0; - int valueMax_ = 255; - int valueStep_ = 1; - bool exposeLoaded_ = false; + QString valueOn_; + QString valueOff_; 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", @@ -43,22 +36,12 @@ 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/items/mqttitemsource.cpp b/src/items/mqttitemsource.cpp new file mode 100644 index 0000000..eb3c11c --- /dev/null +++ b/src/items/mqttitemsource.cpp @@ -0,0 +1,205 @@ +#include "mqttitemsource.h" + +#include +#include +#include + +MqttItemSource::MqttItemSource(QObject *parent) + : ItemSource(parent) +{ +} + +MqttItemSource::~MqttItemSource() +{ +} + +void MqttItemSource::start(const QJsonObject& settings) +{ + baseTopicName_ = settings["BaseTopic"].toString("zigbee2mqtt"); + + connect(&client_, &QMqttClient::stateChanged, this, &MqttItemSource::onClientStateChanged); + connect(&client_, &QMqttClient::errorChanged, this, &MqttItemSource::onClientError); + + client_.setHostname(settings["Host"].toString("127.0.0.1")); + client_.setPort(settings["Port"].toInt(1883)); + 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_.connectToHost(); + + QJsonArray itemsArray = settings["Items"].toArray(); + + for(QJsonValueRef itemRef : itemsArray) + { + QJsonObject itemObject = itemRef.toObject(); + if(!itemObject.contains("Topic")) + continue; + + MqttItemConfig config; + config.topic = itemObject["Topic"].toString(); + config.name = itemObject.contains("Name") ? itemObject["Name"].toString() : config.topic; + config.valueKey = itemObject["ValueKey"].toString("state"); + config.valueOn = itemObject["ValueOn"].toString("ON"); + config.valueOff = itemObject["ValueOff"].toString("OFF"); + + // Determine value type + QString valueTypeStr = itemObject["ValueType"].toString("bool"); + if(valueTypeStr == "uint") + config.valueType = ITEM_VALUE_UINT; + else + config.valueType = ITEM_VALUE_BOOL; + + config.id = qHash(baseTopicName_ + "/" + config.topic); + + // Create the item + config.item = std::make_shared(config.id, config.name, 0); + config.item->setTopic(config.topic); + config.item->setValueKey(config.valueKey); + config.item->setBaseTopic(baseTopicName_); + config.item->setValueOn(config.valueOn); + config.item->setValueOff(config.valueOff); + config.item->setMqttClient(&client_); + + items_.push_back(config); + } +} + +MqttItemSource::MqttItemConfig* MqttItemSource::findConfig(const QString& topic) +{ + for(MqttItemConfig& config : items_) + { + if(baseTopicName_ + "/" + config.topic == topic) + return &config; + } + return nullptr; +} + +void MqttItemSource::onClientError(QMqttClient::ClientError error) +{ + qWarning() << "MqttItemSource Client error:" << error; +} + +void MqttItemSource::onClientStateChanged(QMqttClient::ClientState state) +{ + if(state == QMqttClient::ClientState::Connected) + { + qInfo() << "MqttItemSource connected to MQTT broker at " << client_.hostname() << client_.port(); + for(MqttItemConfig& config : items_) + { + // Subscribe to state topic to receive updates from devices + QString stateTopic = baseTopicName_ + "/" + config.topic; + qDebug() << "MqttItemSource subscribing to" << stateTopic; + QMqttSubscription* subscription = client_.subscribe(stateTopic); + if(subscription) + { + connect(subscription, &QMqttSubscription::messageReceived, this, &MqttItemSource::onMessageReceived); + } + } + } + else if(state == QMqttClient::ClientState::Disconnected) + { + qWarning() << "MqttItemSource lost connection to MQTT broker"; + } + else if(state == QMqttClient::ClientState::Connecting) + { + qInfo() << "MqttItemSource connecting to MQTT broker at " << client_.hostname() << client_.port(); + } +} + +void MqttItemSource::onMessageReceived(const QMqttMessage& message) +{ + MqttItemConfig* config = findConfig(message.topic().name()); + if(!config) + return; + + QJsonDocument doc = QJsonDocument::fromJson(message.payload()); + if(doc.isObject()) + { + QJsonObject obj = doc.object(); + QString valueKey = config->valueKey; + + if(obj.contains(valueKey)) + { + uint8_t newValue = 0; + + // Handle different value types + if(config->valueType == ITEM_VALUE_UINT) + { + // Numeric value (brightness, etc.) + newValue = obj[valueKey].toInt(0); + } + else + { + // Binary value (state on/off, etc.) + QString value = obj[valueKey].toString(); + if(value == config->valueOn || value == "ON" || value == "true") + newValue = 1; + else if(value == config->valueOff || value == "OFF" || value == "false") + newValue = 0; + } + + // Only update if value changed + if(config->item->getValue() != newValue) + { + ItemUpdateRequest update; + update.type = ITEM_UPDATE_BACKEND; + update.payload = *config->item; + update.payload.setValueData(newValue); + update.changes.value = true; + config->item->requestUpdate(update); + } + } + } +} + +void MqttItemSource::refresh() +{ + std::vector requests; + + for(MqttItemConfig& config : items_) + { + ItemAddRequest request; + request.type = ITEM_UPDATE_BACKEND; + request.payload = config.item; + request.changes = ItemFieldChanges(true); + requests.push_back(request); + } + + gotItems(requests); +} + +void MqttItemSource::store(QJsonObject& json) +{ + json["Host"] = client_.hostname(); + json["Port"] = client_.port(); + json["BaseTopic"] = baseTopicName_; + if(client_.username() != "") + json["User"] = client_.username(); + if(client_.password() != "") + json["Password"] = client_.password(); + + QJsonArray itemsArray; + for(const MqttItemConfig& config : items_) + { + QJsonObject itemObject; + itemObject["Name"] = config.name; + itemObject["Topic"] = config.topic; + itemObject["ValueKey"] = config.valueKey; + itemObject["ValueOn"] = config.valueOn; + itemObject["ValueOff"] = config.valueOff; + if(config.valueType == ITEM_VALUE_UINT) + itemObject["ValueType"] = "uint"; + else + itemObject["ValueType"] = "bool"; + itemsArray.append(itemObject); + } + json["Items"] = itemsArray; +} + +QMqttClient* MqttItemSource::getClient() +{ + return &client_; +} \ No newline at end of file diff --git a/src/items/mqttitemsource.h b/src/items/mqttitemsource.h new file mode 100644 index 0000000..080e8a8 --- /dev/null +++ b/src/items/mqttitemsource.h @@ -0,0 +1,34 @@ +#ifndef MQTTITEMSOURCE_H +#define MQTTITEMSOURCE_H + +#include +#include +#include +#include +#include + +#include "itemsource.h" +#include "mqttitem.h" + +class MqttItemSource : public ItemSource +{ + Q_OBJECT + + QString baseTopicName_; + QMqttClient client_; + +private slots: + void onClientStateChanged(QMqttClient::ClientState state); + void onMessageReceived(const QMqttMessage& message); + void onClientError(QMqttClient::ClientError error); + +public: + explicit MqttItemSource(QObject *parent = nullptr); + virtual ~MqttItemSource() override; + virtual void refresh() override; + void start(const QJsonObject& settings); + void store(QJsonObject& json); + QMqttClient* getClient(); +}; + +#endif // MQTTITEMSOURCE_H \ No newline at end of file diff --git a/src/mqttclient.cpp b/src/mqttclient.cpp index eb504a6..fcc4692 100644 --- a/src/mqttclient.cpp +++ b/src/mqttclient.cpp @@ -16,12 +16,11 @@ 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_3_1); + client->setProtocolVersion(QMqttClient::MQTT_5_0); client->connectToHost(); } @@ -84,17 +83,13 @@ void MqttClient::unsubscribe(MqttClient::Subscription* subscription) void MqttClient::unsubscribe(QString topic) { + assert(!subscriptions.contains(topic)); MqttClient::Subscription* sub = subscriptions[topic]; - if(!sub) - { - qWarning()<<"MqttClient: Trying to unsubscribe from unkown topic:"<ref > 0) return; - qDebug()<<"MqttClient: unsubscibeing"<subscription->topic().filter(); + qDebug()<<"MqttClient: unsubscibeing"<subscription->topic(); client->unsubscribe(sub->subscription->topic()); subscriptions.erase(topic); delete sub; diff --git a/src/sensors/mqttsensorsource.cpp b/src/sensors/mqttsensorsource.cpp index be4ec20..63b5c02 100644 --- a/src/sensors/mqttsensorsource.cpp +++ b/src/sensors/mqttsensorsource.cpp @@ -47,6 +47,7 @@ void MqttSensorSource::onClientStateChanged(QMqttClient::ClientState state) { for(SensorSubscription& sensor : sensors) { + qDebug()<<"MQTT subscribeing to"<getBaseTopic() + "/" + sensor.topic; sensor.subscription = client->subscribe(client->getBaseTopic() + "/" + sensor.topic); connect(sensor.subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived); } @@ -57,7 +58,7 @@ void MqttSensorSource::onClientStateChanged(QMqttClient::ClientState state) { if(sensor.subscription) { - client->unsubscribe(client->getBaseTopic() + "/" + sensor.topic); + client->unsubscribe(sensor.topic); sensor.subscription = nullptr; } } @@ -147,30 +148,6 @@ void MqttSensorSource::onMessageReceived(const QMqttMessage& message) sensor.field = obj["voc"].toDouble(0); stateChanged(sensor); } - - if(obj.contains("power")) - { - sensor.name = baseName + " Power"; - sensor.type = Sensor::TYPE_POWER; - sensor.field = obj["Power"].toDouble(0); - stateChanged(sensor); - } - - if(obj.contains("energy")) - { - sensor.name = baseName + " Energy"; - sensor.type = Sensor::TYPE_ENERGY_USE; - sensor.field = obj["energy"].toDouble(0); - stateChanged(sensor); - } - - if(obj.contains("voltage")) - { - sensor.name = baseName + " Voltage"; - sensor.type = Sensor::TYPE_VOLTAGE; - sensor.field = obj["voltage"].toDouble(0); - stateChanged(sensor); - } } } @@ -190,6 +167,6 @@ void MqttSensorSource::store(QJsonObject& json) MqttSensorSource::~MqttSensorSource() { for(SensorSubscription& sub : sensors) - client->unsubscribe(client->getBaseTopic() + "/" + sub.topic); + client->unsubscribe(sub.topic); } diff --git a/src/sensors/sensor.h b/src/sensors/sensor.h index d24090a..86fa5cb 100644 --- a/src/sensors/sensor.h +++ b/src/sensors/sensor.h @@ -22,9 +22,6 @@ public: TYPE_FORMALDEHYD, TYPE_PM25, TYPE_TOTAL_VOC, - TYPE_ENERGY_USE, - TYPE_POWER, - TYPE_VOLTAGE, TYPE_LOWBATTERY = 128, TYPE_SHUTDOWN_IMMINENT = 251, TYPE_OCUPANCY, @@ -148,12 +145,6 @@ public: return "ppb"; case TYPE_SUN_ALTITUDE: return "°"; - case TYPE_POWER: - return "W"; - case TYPE_ENERGY_USE: - return "kWh"; - case TYPE_VOLTAGE: - return "V"; default: return ""; } diff --git a/src/service/server.cpp b/src/service/server.cpp index b979413..13fc7f3 100644 --- a/src/service/server.cpp +++ b/src/service/server.cpp @@ -107,7 +107,7 @@ void Server::itemUpdated(ItemUpdateRequest update) { QJsonArray items; QJsonObject itemjson; - update.payload.storeWithChanges(itemjson, update.changes); + update.payload.store(itemjson); items.append(itemjson); QJsonObject json = createMessage("ItemUpdate", items); json["FullList"] = false; diff --git a/src/service/tcpclient.cpp b/src/service/tcpclient.cpp index 6e00e2d..afcb473 100644 --- a/src/service/tcpclient.cpp +++ b/src/service/tcpclient.cpp @@ -120,7 +120,7 @@ void TcpClient::itemUpdated(ItemUpdateRequest update) { QJsonArray items; QJsonObject itemjson; - update.payload.storeWithChanges(itemjson, update.changes); + update.payload.store(itemjson); items.append(itemjson); QJsonObject json = createMessage("ItemUpdate", items); json["FullList"] = false; diff --git a/src/ui/itemscrollbox.cpp b/src/ui/itemscrollbox.cpp index f704996..55ba900 100644 --- a/src/ui/itemscrollbox.cpp +++ b/src/ui/itemscrollbox.cpp @@ -1,7 +1,6 @@ #include "itemscrollbox.h" #include "ui_relayscrollbox.h" #include -#include #include ItemScrollBox::ItemScrollBox(QWidget *parent) : @@ -132,7 +131,7 @@ void ItemScrollBox::onItemUpdate(const ItemUpdateRequest& update) { if(widget->controles(update.payload)) { - qDebug()<<"ItemUpdate with group change for item"< item = widget->getItem(); removeItemFromTabs(update.payload); addItemToTabs(item); @@ -149,7 +148,6 @@ void ItemScrollBox::ensureTabExists(const QString& groupName) tab.scroller->setWidgetResizable(true); tab.scroller->setFrameShape(QFrame::NoFrame); tab.scroller->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - QScroller::grabGesture(tab.scroller->viewport(), QScroller::LeftMouseButtonGesture); tab.content = new QWidget(tab.scroller); QVBoxLayout* scrollLayout = new QVBoxLayout(tab.content); diff --git a/src/ui/itemwidget.cpp b/src/ui/itemwidget.cpp index 336ffb2..de7544e 100644 --- a/src/ui/itemwidget.cpp +++ b/src/ui/itemwidget.cpp @@ -2,7 +2,6 @@ #include "ui_itemwidget.h" #include -#include #include #include #include "itemsettingsdialog.h" @@ -21,28 +20,15 @@ ItemWidget::ItemWidget(std::weak_ptr item, bool noGroupEdit, QWidget *pare { ui->horizontalSpacer->changeSize(0,0); ui->checkBox->hide(); - ui->comboBox->hide(); } else if(workingItem->getValueType() == ITEM_VALUE_NO_VALUE) { ui->checkBox->hide(); ui->slider->hide(); - ui->comboBox->hide(); - } - else if(workingItem->getValueType() == ITEM_VALUE_ENUM) - { - ui->slider->hide(); - ui->checkBox->hide(); - QStringList list; - for(const QString& name : workingItem->getValueNames()) - list.append(name); - ui->comboBox->addItems(list); - ui->comboBox->setCurrentIndex(workingItem->getValue()); } else { ui->slider->hide(); - ui->comboBox->hide(); } ui->checkBox->setChecked(workingItem->getValue()); @@ -51,8 +37,6 @@ ItemWidget::ItemWidget(std::weak_ptr item, bool noGroupEdit, QWidget *pare if(workingItem->getValueType() == ITEM_VALUE_UINT) connect(ui->slider, &QSlider::valueChanged, this, &ItemWidget::moveToValue); - else if(workingItem->getValueType() == ITEM_VALUE_ENUM) - connect(ui->comboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ItemWidget::moveToValue); else connect(ui->checkBox, &QCheckBox::toggled, this, &ItemWidget::moveToState); connect(ui->pushButton, &QPushButton::clicked, this, &ItemWidget::showSettingsDialog); @@ -100,7 +84,6 @@ void ItemWidget::disable() ui->checkBox->setEnabled(false); ui->label->setEnabled(false); ui->slider->setEnabled(false); - ui->comboBox->setEnabled(false); ui->pushButton_Remove->setEnabled(false); } @@ -132,16 +115,6 @@ std::weak_ptr ItemWidget::getItem() void ItemWidget::onItemUpdated(ItemUpdateRequest update) { - if(update.changes.valueNames) - { - ui->comboBox->blockSignals(true); - ui->comboBox->clear(); - QStringList list; - for(const QString& name : update.payload.getValueNames()) - list.append(name); - ui->comboBox->addItems(list); - ui->comboBox->blockSignals(false); - } stateChanged(update.payload.getValue()); } @@ -153,9 +126,6 @@ void ItemWidget::stateChanged(int state) ui->checkBox->blockSignals(true); ui->checkBox->setChecked(state); ui->checkBox->blockSignals(false); - ui->comboBox->blockSignals(true); - ui->comboBox->setCurrentIndex(state); - ui->comboBox->blockSignals(false); } ItemWidget::~ItemWidget() diff --git a/src/ui/itemwidget.ui b/src/ui/itemwidget.ui index a1e4bf0..96b625e 100644 --- a/src/ui/itemwidget.ui +++ b/src/ui/itemwidget.ui @@ -59,16 +59,6 @@ - - - - - 0 - 0 - - - - diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bddc2f4..28c8792 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,35 +6,167 @@ enable_testing() # Find Qt packages for tests find_package(Qt6 COMPONENTS Core Gui Widgets Multimedia Test REQUIRED) -# Add test executables - link to static library instead of compiling sources -add_executable(test_item unit/items/test_item.cpp) -add_executable(test_sensor unit/sensors/test_sensor.cpp) -add_executable(test_actor unit/actors/test_actor.cpp) -add_executable(test_itemstore unit/items/test_itemstore.cpp) -add_executable(test_itemloadersource unit/items/test_itemloadersource.cpp) -add_executable(test_mqttitem unit/items/test_mqttitem.cpp) -add_executable(test_tcp unit/service/test_tcp.cpp) +# Define common sources needed by all tests - include only what's actually needed for basic testing +set(COMMON_TEST_SOURCES + ../src/items/item.h + ../src/items/item.cpp + ../src/sensors/sensor.h + ../src/sensors/sensor.cpp + ../src/programmode.h + ../src/programmode.cpp + ../src/microcontroller.h + ../src/microcontroller.cpp + ../src/actors/actor.h + ../src/actors/actor.cpp + ../src/actors/factoractor.h + ../src/actors/factoractor.cpp + ../src/actors/polynomalactor.h + ../src/actors/polynomalactor.cpp + ../src/actors/sensoractor.h + ../src/actors/sensoractor.cpp + ../src/actors/timeractor.h + ../src/actors/timeractor.cpp + ../src/items/relay.h + ../src/items/relay.cpp + ../src/items/messageitem.h + ../src/items/messageitem.cpp + ../src/items/systemitem.h + ../src/items/systemitem.cpp + ../src/items/auxitem.h + ../src/items/auxitem.cpp + ../src/items/poweritem.h + ../src/items/poweritem.cpp + ../src/items/rgbitem.h + ../src/items/rgbitem.cpp + ../src/actors/alarmtime.h + ../src/actors/alarmtime.cpp + ../src/actors/regulator.h + ../src/actors/regulator.cpp + ../src/items/itemsource.h + ../src/items/itemsource.cpp + ../src/items/itemstore.h + ../src/items/itemstore.cpp + ../src/items/itemloadersource.h + ../src/items/itemloadersource.cpp + ../src/items/mqttitem.h + ../src/items/mqttitem.cpp + ../src/mqttclient.cpp +) -# Link all tests to static library -foreach(test test_item test_sensor test_actor test_itemstore test_itemloadersource test_mqttitem test_tcp) - target_link_libraries(${test} - smartvos_core - Qt6::Core - Qt6::Gui - Qt6::Widgets - Qt6::Multimedia - Qt6::Network - Qt6::WebSockets - Qt6::Mqtt - Qt6::Test - ) - - # Include paths - the static library already has the correct include paths - target_include_directories(${test} PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../src - ${Qt6Gui_PRIVATE_INCLUDE_DIRS} - ) -endforeach() +# Add test executables - compile all needed sources into each test +add_executable(test_item unit/items/test_item.cpp ${COMMON_TEST_SOURCES}) +add_executable(test_sensor unit/sensors/test_sensor.cpp ${COMMON_TEST_SOURCES}) +add_executable(test_actor unit/actors/test_actor.cpp ${COMMON_TEST_SOURCES}) +add_executable(test_itemstore unit/items/test_itemstore.cpp ${COMMON_TEST_SOURCES}) +add_executable(test_itemloadersource unit/items/test_itemloadersource.cpp ${COMMON_TEST_SOURCES}) +add_executable(test_tcp unit/service/test_tcp.cpp ${COMMON_TEST_SOURCES} + ../src/service/service.h + ../src/service/service.cpp + ../src/service/server.h + ../src/service/server.cpp + ../src/service/tcpserver.h + ../src/service/tcpserver.cpp + ../src/service/tcpclient.h + ../src/service/tcpclient.cpp +) + +# Link libraries for test_item +target_link_libraries(test_item + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Multimedia + Qt6::Mqtt + Qt6::Test +) + +# Include paths for source files +target_include_directories(test_item PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ${Qt6Gui_PRIVATE_INCLUDE_DIRS} +) + +# Link libraries for test_sensor +target_link_libraries(test_sensor + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Multimedia + Qt6::Mqtt + Qt6::Test +) + +# Include paths for source files +target_include_directories(test_sensor PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ${Qt6Gui_PRIVATE_INCLUDE_DIRS} +) + +# Link libraries for test_actor +target_link_libraries(test_actor + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Multimedia + Qt6::Mqtt + Qt6::Test +) + +# Include paths for source files +target_include_directories(test_actor PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ${Qt6Gui_PRIVATE_INCLUDE_DIRS} +) + +# Link libraries for test_itemstore +target_link_libraries(test_itemstore + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Multimedia + Qt6::Mqtt + Qt6::Test +) + +# Include paths for source files +target_include_directories(test_itemstore PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ${Qt6Gui_PRIVATE_INCLUDE_DIRS} +) + +# Link libraries for test_itemloadersource +target_link_libraries(test_itemloadersource + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Multimedia + Qt6::Mqtt + Qt6::Test +) + +# Include paths for source files +target_include_directories(test_itemloadersource PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ${Qt6Gui_PRIVATE_INCLUDE_DIRS} +) + +# Link libraries for test_tcp +target_link_libraries(test_tcp + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Multimedia + Qt6::Network + Qt6::WebSockets + Qt6::Mqtt + Qt6::Test +) + +# Include paths for source files +target_include_directories(test_tcp PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ${Qt6Gui_PRIVATE_INCLUDE_DIRS} +) # Add tests to CTest add_test(NAME test_item COMMAND test_item) @@ -42,5 +174,4 @@ add_test(NAME test_sensor COMMAND test_sensor) add_test(NAME test_actor COMMAND test_actor) add_test(NAME test_itemstore COMMAND test_itemstore) add_test(NAME test_itemloadersource COMMAND test_itemloadersource) -add_test(NAME test_mqttitem COMMAND test_mqttitem) -add_test(NAME test_tcp COMMAND test_tcp) \ No newline at end of file +add_test(NAME test_tcp COMMAND test_tcp) diff --git a/tests/unit/items/test_mqttitem.cpp b/tests/unit/items/test_mqttitem.cpp deleted file mode 100644 index 27a8940..0000000 --- a/tests/unit/items/test_mqttitem.cpp +++ /dev/null @@ -1,306 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "items/mqttitem.h" -#include "mqttclient.h" -#include "programmode.h" - -class TestMqttItem : public QObject -{ - Q_OBJECT - -private slots: - void initTestCase() - { - // Setup for all tests - // Try to load config and connect to MQTT broker if configured - QString settingsPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/shinterface.json"; - - QJsonObject json; - if (QFile::exists(settingsPath)) { - QFile file(settingsPath); - if (file.open(QIODevice::ReadOnly)) { - QByteArray data = file.readAll(); - file.close(); - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(data, &error); - if (error.error == QJsonParseError::NoError) { - json = doc.object(); - } - } - } - - QJsonObject mqttJson = json["Mqtt"].toObject(); - QString host = mqttJson["Host"].toString(); - int port = mqttJson["Port"].toInt(1883); - - // If MQTT is configured with a host, try to connect - static std::shared_ptr mqttClient; - if (!host.isEmpty()) { - qDebug() << "MQTT configured:" << host << port; - mqttClient = std::make_shared(); - mqttClient->start(mqttJson); - // Give it a moment to connect - QTest::qWait(1000); - - // Check if connected or connecting - auto qClient = mqttClient->getClient(); - if (qClient && (qClient->state() == QMqttClient::Connected || - qClient->state() == QMqttClient::Connecting)) { - qDebug() << "MQTT connected/connecting, using client"; - MqttItem::client = mqttClient; - } else { - qDebug() << "MQTT connection failed, using UI_ONLY mode"; - programMode = PROGRAM_MODE_UI_ONLY; - } - } else { - qDebug() << "No MQTT host configured, using UI_ONLY mode"; - programMode = PROGRAM_MODE_UI_ONLY; - } - } - - void testMqttItemCreation() - { - MqttItem item("test_mqtt", 0); - QCOMPARE(item.getName(), QString("test_mqtt")); - QVERIFY(item.getTopic().isEmpty()); - QVERIFY(item.getValueKey() == "state"); - } - - void testMqttItemSetTopic() - { - MqttItem item("test_mqtt", 0); - item.setTopic("my_device"); - item.setValueKey("state"); - - QVERIFY(item.getTopic() == "my_device"); - QVERIFY(item.getValueKey() == "state"); - } - - void testMqttItemSetValueType() - { - MqttItem item("test_mqtt", 0); - - // Default should be BOOL - QVERIFY(item.getValueType() == ITEM_VALUE_BOOL); - - // Set to UINT - item.setValueType(ITEM_VALUE_UINT); - QVERIFY(item.getValueType() == ITEM_VALUE_UINT); - - // Set to ENUM - item.setValueType(ITEM_VALUE_ENUM); - QVERIFY(item.getValueType() == ITEM_VALUE_ENUM); - } - - void testMqttItemValueNames() - { - MqttItem item("test_mqtt", 0); - - // Initially empty - QVERIFY(item.getValueNames().empty()); - - // Set value names - std::vector names = {"off", "heat", "cool"}; - item.setValueNames(names); - - auto storedNames = item.getValueNames(); - QVERIFY(storedNames.size() == 3); - QVERIFY(storedNames[0] == "off"); - QVERIFY(storedNames[1] == "heat"); - QVERIFY(storedNames[2] == "cool"); - } - - void testValueNameConversion() - { - MqttItem item("test_mqtt", 0); - - // Set value names for enum - std::vector names = {"off", "heat", "cool", "auto"}; - item.setValueNames(names); - item.setValueType(ITEM_VALUE_ENUM); - - // Test name to index - QVERIFY(item.valueNameToIndex("heat") == 1); - QVERIFY(item.valueNameToIndex("cool") == 2); - QVERIFY(item.valueNameToIndex("unknown") == -1); - - // Test index to name - QVERIFY(item.indexToValueName(0) == "off"); - QVERIFY(item.indexToValueName(3) == "auto"); - 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); - item.setTopic("my_device"); - item.setValueKey("state"); - item.setValueOn("ON"); - item.setValueOff("OFF"); - - QJsonObject json; - item.store(json); - - QVERIFY(json["Type"] == "Mqtt"); - QVERIFY(json["Topic"] == "my_device"); - QVERIFY(json["ValueKey"] == "state"); - QVERIFY(json["ValueOn"] == "ON"); - QVERIFY(json["ValueOff"] == "OFF"); - } - - void testJsonDeserialization() - { - QJsonObject json; - json["Type"] = "Mqtt"; - json["ItemId"] = 100; - json["Name"] = "loaded_mqtt"; - json["Topic"] = "test_device"; - json["ValueKey"] = "state"; - json["ValueOn"] = "ON"; - json["ValueOff"] = "OFF"; - json["Value"] = 1; - - MqttItem item; - item.load(json); - - QVERIFY(item.getTopic() == "test_device"); - QVERIFY(item.getValueKey() == "state"); - QVERIFY(item.getValueOn() == "ON"); - 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); - } - - void cleanupTestCase() - { - // Cleanup after all tests - } -}; - -QTEST_APPLESS_MAIN(TestMqttItem) - -#include "test_mqttitem.moc" \ No newline at end of file