From cd64cbe08e1a2ec34670a9bff072da9e4059dc7c Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 13 Apr 2026 14:08:24 +0200 Subject: [PATCH 01/11] UI: fix finger scolling in itemscrollbox tabs --- src/ui/itemscrollbox.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/itemscrollbox.cpp b/src/ui/itemscrollbox.cpp index 55ba900..f704996 100644 --- a/src/ui/itemscrollbox.cpp +++ b/src/ui/itemscrollbox.cpp @@ -1,6 +1,7 @@ #include "itemscrollbox.h" #include "ui_relayscrollbox.h" #include +#include #include ItemScrollBox::ItemScrollBox(QWidget *parent) : @@ -131,7 +132,7 @@ void ItemScrollBox::onItemUpdate(const ItemUpdateRequest& update) { if(widget->controles(update.payload)) { - qDebug()<<"ItemUpdate with group change"; + qDebug()<<"ItemUpdate with group change for item"< item = widget->getItem(); removeItemFromTabs(update.payload); addItemToTabs(item); @@ -148,6 +149,7 @@ 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); From b0792d32dbe9a99bc1147605acb3e97820c67b7b Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 13 Apr 2026 14:09:34 +0200 Subject: [PATCH 02/11] Service: Only sent item changes in item updates --- src/service/server.cpp | 2 +- src/service/tcpclient.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/service/server.cpp b/src/service/server.cpp index 13fc7f3..b979413 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.store(itemjson); + update.payload.storeWithChanges(itemjson, update.changes); items.append(itemjson); QJsonObject json = createMessage("ItemUpdate", items); json["FullList"] = false; diff --git a/src/service/tcpclient.cpp b/src/service/tcpclient.cpp index afcb473..6e00e2d 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.store(itemjson); + update.payload.storeWithChanges(itemjson, update.changes); items.append(itemjson); QJsonObject json = createMessage("ItemUpdate", items); json["FullList"] = false; From d69bfb58b9bef2c5bb0065529b3b3161497abaf6 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 13 Apr 2026 14:09:58 +0200 Subject: [PATCH 03/11] Fix mqtt items asserting on client instances --- src/items/itemloadersource.cpp | 1 + src/items/mqttitem.cpp | 6 ++++-- src/mqttclient.cpp | 8 ++++++-- src/sensors/mqttsensorsource.cpp | 5 ++--- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/items/itemloadersource.cpp b/src/items/itemloadersource.cpp index db5478a..e2d1e68 100644 --- a/src/items/itemloadersource.cpp +++ b/src/items/itemloadersource.cpp @@ -26,6 +26,7 @@ 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 a6b30e5..76c5264 100644 --- a/src/items/mqttitem.cpp +++ b/src/items/mqttitem.cpp @@ -5,6 +5,7 @@ #include #include "mqttclient.h" +#include "programmode.h" MqttItem::MqttItem(QString name, uint8_t value, QObject *parent) : Item(0, name, value, parent), @@ -15,9 +16,10 @@ MqttItem::MqttItem(QString name, uint8_t value, QObject *parent) { hashId(); std::shared_ptr workClient = client.lock(); - assert(workClient); + assert(workClient || programMode == PROGRAM_MODE_UI_ONLY); - connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged); + if(workClient) + connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged); } MqttItem::~MqttItem() diff --git a/src/mqttclient.cpp b/src/mqttclient.cpp index fcc4692..ab61375 100644 --- a/src/mqttclient.cpp +++ b/src/mqttclient.cpp @@ -83,13 +83,17 @@ 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(); + qDebug()<<"MqttClient: unsubscibeing"<subscription->topic().filter(); client->unsubscribe(sub->subscription->topic()); subscriptions.erase(topic); delete sub; diff --git a/src/sensors/mqttsensorsource.cpp b/src/sensors/mqttsensorsource.cpp index 63b5c02..0853abf 100644 --- a/src/sensors/mqttsensorsource.cpp +++ b/src/sensors/mqttsensorsource.cpp @@ -47,7 +47,6 @@ 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); } @@ -58,7 +57,7 @@ void MqttSensorSource::onClientStateChanged(QMqttClient::ClientState state) { if(sensor.subscription) { - client->unsubscribe(sensor.topic); + client->unsubscribe(client->getBaseTopic() + "/" + sensor.topic); sensor.subscription = nullptr; } } @@ -167,6 +166,6 @@ void MqttSensorSource::store(QJsonObject& json) MqttSensorSource::~MqttSensorSource() { for(SensorSubscription& sub : sensors) - client->unsubscribe(sub.topic); + client->unsubscribe(client->getBaseTopic() + "/" + sub.topic); } From 6c6fc11439f1c17a39cb810a6be40f4bfb27e4d3 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 13 Apr 2026 14:20:11 +0200 Subject: [PATCH 04/11] Change the name of the project to smartvos and use a static libary for the core --- CMakeLists.txt | 189 ++++++++++++++++++++++++++----------------- tests/CMakeLists.txt | 189 +++++++------------------------------------ 2 files changed, 142 insertions(+), 236 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ca60d65..6508688 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 4.0) -project(SHinterface VERSION 1.0 LANGUAGES CXX) +project(smartvos VERSION 1.0 LANGUAGES CXX) # Set C++ standard set(CMAKE_CXX_STANDARD 20) @@ -27,88 +27,113 @@ 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(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 +add_executable(smartvos 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 @@ -148,7 +173,7 @@ target_sources(SHinterface ) # Add UI files -target_sources(SHinterface +target_sources(smartvos PRIVATE src/ui/mainwindow.ui src/ui/itemwidget.ui @@ -169,13 +194,14 @@ target_sources(SHinterface ) # Add resource file -target_sources(SHinterface +target_sources(smartvos PRIVATE resources.qrc ) -# Link libraries -target_link_libraries(SHinterface +# Link libraries - link to static library plus UI-specific dependencies +target_link_libraries(smartvos + smartvos_core Qt6::Core Qt6::Gui Qt6::Widgets @@ -190,3 +216,16 @@ target_link_libraries(SHinterface # 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/tests/CMakeLists.txt b/tests/CMakeLists.txt index 28c8792..dabb4fd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,167 +6,34 @@ enable_testing() # Find Qt packages for tests find_package(Qt6 COMPONENTS Core Gui Widgets Multimedia Test REQUIRED) -# 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 -) +# 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_tcp unit/service/test_tcp.cpp) -# 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} -) +# Link all tests to static library +foreach(test test_item test_sensor test_actor test_itemstore test_itemloadersource 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 tests to CTest add_test(NAME test_item COMMAND test_item) @@ -174,4 +41,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_tcp COMMAND test_tcp) +add_test(NAME test_tcp COMMAND test_tcp) \ No newline at end of file From e881472e7c6b0cd9c241dd74fe8558bd5936faf4 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 13 Apr 2026 14:20:45 +0200 Subject: [PATCH 05/11] Add .desktop file --- smartvos.desktop | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 smartvos.desktop diff --git a/smartvos.desktop b/smartvos.desktop new file mode 100644 index 0000000..adc785e --- /dev/null +++ b/smartvos.desktop @@ -0,0 +1,10 @@ +[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 From 3794e0031b03ea34545cb02b07dc88b0d5ac638c Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 13 Apr 2026 15:13:47 +0200 Subject: [PATCH 06/11] Finis support for ENUM item types --- src/items/item.cpp | 52 +++++++++++++++++++++++++++++++++++++++++++ src/items/item.h | 8 ++++++- src/ui/itemwidget.cpp | 27 ++++++++++++++++++++++ src/ui/itemwidget.ui | 10 +++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/items/item.cpp b/src/items/item.cpp index b902c32..14d31b3 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -60,6 +60,13 @@ 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) @@ -87,6 +94,19 @@ 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; @@ -120,6 +140,8 @@ 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; } @@ -148,6 +170,33 @@ 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, @@ -205,6 +254,7 @@ Item& Item::operator=(const ItemData& other) itemId_ = other.id(); hidden_ = other.isHidden(); groupName_ = other.getGroupName(); + valueNames_ = other.getValueNames(); return *this; } @@ -248,6 +298,8 @@ 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 b12575c..4a70fae 100644 --- a/src/items/item.h +++ b/src/items/item.h @@ -71,6 +71,10 @@ 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; @@ -130,6 +134,7 @@ struct ItemFieldChanges bool type :1; bool groupName :1; bool actors :1; + bool valueNames :1; ItemFieldChanges(bool defaultVal = false) { name = defaultVal; @@ -138,10 +143,11 @@ struct ItemFieldChanges type = defaultVal; groupName = defaultVal; actors = false; + valueNames = defaultVal; } inline bool isNone() const { - return !name && !value && !hidden && !type && !groupName && !actors; + return !name && !value && !hidden && !type && !groupName && !actors && !valueNames; } }; diff --git a/src/ui/itemwidget.cpp b/src/ui/itemwidget.cpp index de7544e..3846911 100644 --- a/src/ui/itemwidget.cpp +++ b/src/ui/itemwidget.cpp @@ -2,6 +2,7 @@ #include "ui_itemwidget.h" #include +#include #include #include #include "itemsettingsdialog.h" @@ -26,6 +27,16 @@ ItemWidget::ItemWidget(std::weak_ptr item, bool noGroupEdit, QWidget *pare ui->checkBox->hide(); ui->slider->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(); @@ -37,6 +48,8 @@ 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); @@ -84,6 +97,7 @@ void ItemWidget::disable() ui->checkBox->setEnabled(false); ui->label->setEnabled(false); ui->slider->setEnabled(false); + ui->comboBox->setEnabled(false); ui->pushButton_Remove->setEnabled(false); } @@ -115,6 +129,16 @@ 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()); } @@ -126,6 +150,9 @@ 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 96b625e..a1e4bf0 100644 --- a/src/ui/itemwidget.ui +++ b/src/ui/itemwidget.ui @@ -59,6 +59,16 @@ + + + + + 0 + 0 + + + + From be303aa85178da5fffb883590dfa851bbf5a933f Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Fri, 17 Apr 2026 18:30:43 +0200 Subject: [PATCH 07/11] Support power, engery and voltage sensors in sensors and mqtt --- src/sensors/mqttsensorsource.cpp | 24 ++++++++++++++++++++++++ src/sensors/sensor.h | 9 +++++++++ 2 files changed, 33 insertions(+) diff --git a/src/sensors/mqttsensorsource.cpp b/src/sensors/mqttsensorsource.cpp index 0853abf..be4ec20 100644 --- a/src/sensors/mqttsensorsource.cpp +++ b/src/sensors/mqttsensorsource.cpp @@ -147,6 +147,30 @@ 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); + } } } diff --git a/src/sensors/sensor.h b/src/sensors/sensor.h index 86fa5cb..d24090a 100644 --- a/src/sensors/sensor.h +++ b/src/sensors/sensor.h @@ -22,6 +22,9 @@ public: TYPE_FORMALDEHYD, TYPE_PM25, TYPE_TOTAL_VOC, + TYPE_ENERGY_USE, + TYPE_POWER, + TYPE_VOLTAGE, TYPE_LOWBATTERY = 128, TYPE_SHUTDOWN_IMMINENT = 251, TYPE_OCUPANCY, @@ -145,6 +148,12 @@ 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 ""; } From cb05c2237e18af87d3f0425d23a06ac2f054d810 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Fri, 17 Apr 2026 18:31:42 +0200 Subject: [PATCH 08/11] Dont show combo box on non-enum items --- src/ui/itemwidget.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/itemwidget.cpp b/src/ui/itemwidget.cpp index 3846911..336ffb2 100644 --- a/src/ui/itemwidget.cpp +++ b/src/ui/itemwidget.cpp @@ -21,11 +21,13 @@ 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) { @@ -40,6 +42,7 @@ ItemWidget::ItemWidget(std::weak_ptr item, bool noGroupEdit, QWidget *pare else { ui->slider->hide(); + ui->comboBox->hide(); } ui->checkBox->setChecked(workingItem->getValue()); From 869067c2f9d82d85c25b7d68ebe97d82f4648300 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Fri, 17 Apr 2026 18:32:41 +0200 Subject: [PATCH 09/11] Test: Add mqttitem unit tests --- tests/CMakeLists.txt | 4 +- tests/unit/items/test_mqttitem.cpp | 306 +++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 tests/unit/items/test_mqttitem.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dabb4fd..bddc2f4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,10 +12,11 @@ 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) # Link all tests to static library -foreach(test test_item test_sensor test_actor test_itemstore test_itemloadersource test_tcp) +foreach(test test_item test_sensor test_actor test_itemstore test_itemloadersource test_mqttitem test_tcp) target_link_libraries(${test} smartvos_core Qt6::Core @@ -41,4 +42,5 @@ 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 diff --git a/tests/unit/items/test_mqttitem.cpp b/tests/unit/items/test_mqttitem.cpp new file mode 100644 index 0000000..27a8940 --- /dev/null +++ b/tests/unit/items/test_mqttitem.cpp @@ -0,0 +1,306 @@ +#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 From 25dd99d8b6b427c7e70f918e34d37a5b91888805 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Fri, 17 Apr 2026 18:33:39 +0200 Subject: [PATCH 10/11] Remove unused mqttitemsource files --- src/items/mqttitemsource.cpp | 205 ----------------------------------- src/items/mqttitemsource.h | 34 ------ 2 files changed, 239 deletions(-) delete mode 100644 src/items/mqttitemsource.cpp delete mode 100644 src/items/mqttitemsource.h diff --git a/src/items/mqttitemsource.cpp b/src/items/mqttitemsource.cpp deleted file mode 100644 index eb3c11c..0000000 --- a/src/items/mqttitemsource.cpp +++ /dev/null @@ -1,205 +0,0 @@ -#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 deleted file mode 100644 index 080e8a8..0000000 --- a/src/items/mqttitemsource.h +++ /dev/null @@ -1,34 +0,0 @@ -#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 From 05e4fb1cc180d690989a7f695b4948e7af7053f4 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Fri, 17 Apr 2026 18:35:16 +0200 Subject: [PATCH 11/11] Auto mqttitem attempt --- src/items/mqttitem.cpp | 239 +++++++++++++++++++++++++++++++++++++++-- src/items/mqttitem.h | 23 +++- src/mqttclient.cpp | 3 +- 3 files changed, 254 insertions(+), 11 deletions(-) diff --git a/src/items/mqttitem.cpp b/src/items/mqttitem.cpp index 76c5264..9c2ea78 100644 --- a/src/items/mqttitem.cpp +++ b/src/items/mqttitem.cpp @@ -1,5 +1,6 @@ #include "mqttitem.h" +#include #include #include #include @@ -26,16 +27,35 @@ MqttItem::~MqttItem() { qDebug()<<__func__; std::shared_ptr workClient = client.lock(); - if(!workClient || topic_.isEmpty() || !subscription) + if(!workClient) return; - workClient->unsubscribe(subscription); + if(subscription) + { + disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived); + workClient->unsubscribe(subscription); + } + + if(devicesSubscription) + { + disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived); + workClient->unsubscribe(devicesSubscription); + } } void MqttItem::onClientStateChanged(QMqttClient::ClientState state) { if(state == QMqttClient::Connected) + { refreshSubscription(); + // Subscribe to bridge/devices to get exposes + std::shared_ptr workClient = client.lock(); + if(workClient && !exposeLoaded_) + { + devicesSubscription = workClient->subscribe(workClient->getBaseTopic() + "/bridge/devices"); + connect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived); + } + } } void MqttItem::refreshSubscription() @@ -57,6 +77,101 @@ void MqttItem::refreshSubscription() connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived); } +void MqttItem::onDevicesMessageReceived(const QMqttMessage& message) +{ + if(exposeLoaded_) + return; + + QJsonDocument doc = QJsonDocument::fromJson(message.payload()); + if(!doc.isArray()) + return; + + QJsonArray devices = doc.array(); + for(const QJsonValue& deviceValue : devices) + { + if(!deviceValue.isObject()) + continue; + + QJsonObject device = deviceValue.toObject(); + QString ieeeAddr = device["ieee_address"].toString(); + + // Check if this device matches our topic (friendly_name) + QString friendlyName = device["friendly_name"].toString(); + if(friendlyName == topic_) + { + loadExposeFromDevice(device); + exposeLoaded_ = true; + + // Unsubscribe from devices topic since we found our device + std::shared_ptr workClient = client.lock(); + if(workClient && devicesSubscription) + { + disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived); + workClient->unsubscribe(devicesSubscription); + devicesSubscription = nullptr; + } + break; + } + } +} + +void MqttItem::loadExposeFromDevice(const QJsonObject& device) +{ + // Get definition - may be null for unsupported devices + QJsonObject definition = device["definition"].toObject(); + if(definition.isEmpty()) + { + qWarning() << "MqttItem" << topic_ << "device has no definition (unsupported)"; + return; + } + + // Get exposes from definition + QJsonArray exposes = definition["exposes"].toArray(); + if(exposes.isEmpty()) + { + qWarning() << "MqttItem" << topic_ << "device has no exposes"; + return; + } + + for(const QJsonValue& exposeValue : exposes) + { + if(!exposeValue.isObject()) + continue; + + QJsonObject expose = exposeValue.toObject(); + QString property = expose["property"].toString(); + + // Check if this expose matches our valueKey + if(property == valueKey_) + { + setFromExpose(expose); + qDebug() << "MqttItem" << topic_ << "detected type" << expose["type"].toString() << "for property" << valueKey_; + return; + } + + // Check if it's a composite type with features + if(expose["type"].toString() == "composite" || expose["type"].toString() == "light") + { + QJsonArray features = expose["features"].toArray(); + for(const QJsonValue& featureValue : features) + { + if(!featureValue.isObject()) + continue; + + QJsonObject feature = featureValue.toObject(); + if(feature["property"].toString() == valueKey_) + { + setFromExpose(feature); + qDebug() << "MqttItem" << topic_ << "detected type" << feature["type"].toString() << "for property" << valueKey_; + return; + } + } + } + } + + qWarning() << "MqttItem" << topic_ << "could not find expose for property" << valueKey_; +} + void MqttItem::onMessageReceived(const QMqttMessage& message) { QJsonDocument doc = QJsonDocument::fromJson(message.payload()); @@ -65,13 +180,32 @@ void MqttItem::onMessageReceived(const QMqttMessage& message) QJsonObject obj = doc.object(); if(obj.contains(getValueKey())) { - QString value = obj[getValueKey()].toString(); + QJsonValue value = obj[getValueKey()]; ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND); req.changes.value = true; - if(value == getValueOn()) - req.payload.setValueData(true); + + if(getValueType() == ITEM_VALUE_UINT) + { + // Numeric value + req.payload.setValueData(value.toInt(0)); + } + else if(getValueType() == ITEM_VALUE_ENUM) + { + // Enum value - find index + QString strValue = value.toString(); + int index = valueNameToIndex(strValue); + if(index >= 0) + req.payload.setValueData(index); + } else - req.payload.setValueData(false); + { + // Binary value + QString strValue = value.toString(); + if(strValue == getValueOn() || strValue == "ON" || strValue == "true") + req.payload.setValueData(true); + else + req.payload.setValueData(false); + } requestUpdate(req); } } @@ -106,6 +240,59 @@ void MqttItem::setValueOff(const QString& valueOff) valueOff_ = valueOff; } +void MqttItem::setValueMin(int min) +{ + valueMin_ = min; +} + +void MqttItem::setValueMax(int max) +{ + valueMax_ = max; +} + +void MqttItem::setValueStep(int step) +{ + valueStep_ = step; +} + +void MqttItem::setValueType(item_value_type_t type) +{ + type_ = type; +} + +void MqttItem::setFromExpose(const QJsonObject& expose) +{ + QString type = expose["type"].toString(); + QString property = expose["property"].toString(); + + setValueKey(property); + + if(type == "binary") + { + type_ = ITEM_VALUE_BOOL; + setValueOn(expose["value_on"].toString("ON")); + setValueOff(expose["value_off"].toString("OFF")); + } + else if(type == "numeric") + { + type_ = ITEM_VALUE_UINT; + setValueMin(expose["value_min"].toInt(0)); + setValueMax(expose["value_max"].toInt(255)); + setValueStep(expose["value_step"].toInt(1)); + } + else if(type == "enum") + { + type_ = ITEM_VALUE_ENUM; + QJsonArray values = expose["values"].toArray(); + std::vector valueNames; + for(const QJsonValue& v : values) + valueNames.push_back(v.toString()); + setValueNames(valueNames); + } + + hashId(); +} + QString MqttItem::getTopic() const { return topic_; @@ -126,6 +313,26 @@ QString MqttItem::getValueOff() const return valueOff_; } +int MqttItem::getValueMin() const +{ + return valueMin_; +} + +int MqttItem::getValueMax() const +{ + return valueMax_; +} + +int MqttItem::getValueStep() const +{ + return valueStep_; +} + +bool MqttItem::getExposeLoaded() const +{ + return exposeLoaded_; +} + void MqttItem::store(QJsonObject& json) { Item::store(json); @@ -134,6 +341,9 @@ void MqttItem::store(QJsonObject& json) json["ValueKey"] = valueKey_; json["ValueOn"] = valueOn_; json["ValueOff"] = valueOff_; + json["ValueMin"] = valueMin_; + json["ValueMax"] = valueMax_; + json["ValueStep"] = valueStep_; } void MqttItem::load(const QJsonObject& json, const bool preserve) @@ -143,6 +353,10 @@ void MqttItem::load(const QJsonObject& json, const bool preserve) valueKey_ = json["ValueKey"].toString("state"); valueOn_ = json["ValueOn"].toString("ON"); valueOff_ = json["ValueOff"].toString("OFF"); + valueMin_ = json["ValueMin"].toInt(0); + valueMax_ = json["ValueMax"].toInt(255); + valueStep_ = json["ValueStep"].toInt(1); + exposeLoaded_ = json["ExposeLoaded"].toBool(false); hashId(); refreshSubscription(); } @@ -156,7 +370,18 @@ void MqttItem::enactValue(uint8_t value) QString fullTopic = workClient->getBaseTopic() + "/" + topic_ + "/set"; QJsonObject payload; - payload[valueKey_] = value ? valueOn_ : valueOff_; + if(getValueType() == ITEM_VALUE_UINT) + { + payload[valueKey_] = static_cast(value); + } + else if(getValueType() == ITEM_VALUE_ENUM) + { + payload[valueKey_] = indexToValueName(value); + } + else + { + payload[valueKey_] = value ? valueOn_ : valueOff_; + } QJsonDocument doc(payload); QByteArray data = doc.toJson(QJsonDocument::Compact); diff --git a/src/items/mqttitem.h b/src/items/mqttitem.h index c092444..f31203c 100644 --- a/src/items/mqttitem.h +++ b/src/items/mqttitem.h @@ -15,15 +15,22 @@ public: private: QString topic_; QString valueKey_; - QString valueOn_; - QString valueOff_; + QString valueOn_ = "ON"; + QString valueOff_ = "OFF"; + int valueMin_ = 0; + int valueMax_ = 255; + int valueStep_ = 1; + bool exposeLoaded_ = false; MqttClient::Subscription* subscription = nullptr; + MqttClient::Subscription* devicesSubscription = nullptr; void hashId(); void refreshSubscription(); void onMessageReceived(const QMqttMessage& message); void onClientStateChanged(QMqttClient::ClientState state); + void onDevicesMessageReceived(const QMqttMessage& message); + void loadExposeFromDevice(const QJsonObject& device); public: explicit MqttItem(QString name = "MqttItem", @@ -36,12 +43,22 @@ public: void setBaseTopic(const QString& baseTopic); void setValueOn(const QString& valueOn); void setValueOff(const QString& valueOff); + void setValueMin(int min); + void setValueMax(int max); + void setValueStep(int step); + void setValueType(item_value_type_t type); + + // Configure from Zigbee2MQTT expose info + void setFromExpose(const QJsonObject& expose); QString getTopic() const; QString getValueKey() const; - QString getBaseTopic() const; QString getValueOn() const; QString getValueOff() const; + int getValueMin() const; + int getValueMax() const; + int getValueStep() const; + bool getExposeLoaded() const; virtual void store(QJsonObject& json) override; virtual void load(const QJsonObject& json, const bool preserve = false) override; diff --git a/src/mqttclient.cpp b/src/mqttclient.cpp index ab61375..eb504a6 100644 --- a/src/mqttclient.cpp +++ b/src/mqttclient.cpp @@ -16,11 +16,12 @@ void MqttClient::start(const QJsonObject& settings) client->setHostname(settings["Host"].toString("127.0.0.1")); client->setPort(settings["Port"].toInt(1883)); + client->setClientId(settings["ClientId"].toString("smartvos")); if(settings.contains("User")) client->setUsername(settings["User"].toString()); if(settings.contains("Password")) client->setPassword(settings["Password"].toString()); - client->setProtocolVersion(QMqttClient::MQTT_5_0); + client->setProtocolVersion(QMqttClient::MQTT_3_1); client->connectToHost(); }