Compare commits
11 commits
eb60f85604
...
05e4fb1cc1
| Author | SHA1 | Date | |
|---|---|---|---|
| 05e4fb1cc1 | |||
| 25dd99d8b6 | |||
| 869067c2f9 | |||
| cb05c2237e | |||
| be303aa851 | |||
| 3794e0031b | |||
| e881472e7c | |||
| 6c6fc11439 | |||
| d69bfb58b9 | |||
| b0792d32db | |||
| cd64cbe08e |
19 changed files with 864 additions and 497 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
cmake_minimum_required(VERSION 4.0)
|
cmake_minimum_required(VERSION 4.0)
|
||||||
|
|
||||||
project(SHinterface VERSION 1.0 LANGUAGES CXX)
|
project(smartvos VERSION 1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
# Set C++ standard
|
# Set C++ standard
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
|
@ -27,33 +27,20 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
# Enable testing framework
|
# Enable testing framework
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
# Add subdirectory for tests
|
# Define shared sources for static library (core sources used by both main and tests)
|
||||||
add_subdirectory(tests)
|
set(SHINTERFACE_CORE_SOURCES
|
||||||
|
src/sensors/mqttsensorsource.h
|
||||||
# Create executable
|
src/sensors/mqttsensorsource.cpp
|
||||||
add_executable(SHinterface
|
src/items/mqttitem.h
|
||||||
src/sensors/mqttsensorsource.h src/sensors/mqttsensorsource.cpp
|
src/items/mqttitem.cpp
|
||||||
src/items/mqttitem.h src/items/mqttitem.cpp
|
src/mqttclient.h
|
||||||
src/mqttclient.h src/mqttclient.cpp
|
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.h
|
||||||
src/microcontroller.cpp
|
src/microcontroller.cpp
|
||||||
src/sun.h
|
src/sun.h
|
||||||
src/sun.cpp
|
src/sun.cpp
|
||||||
src/programmode.h
|
src/programmode.h
|
||||||
src/programmode.cpp
|
src/programmode.cpp
|
||||||
src/pipewire.h
|
|
||||||
src/pipewire.cpp
|
|
||||||
|
|
||||||
src/service/service.h
|
src/service/service.h
|
||||||
src/service/service.cpp
|
src/service/service.cpp
|
||||||
|
|
@ -108,6 +95,44 @@ target_sources(SHinterface
|
||||||
src/items/fixeditemsource.cpp
|
src/items/fixeditemsource.cpp
|
||||||
src/items/itemstore.h
|
src/items/itemstore.h
|
||||||
src/items/itemstore.cpp
|
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
|
||||||
|
src/main.cpp
|
||||||
|
src/mainobject.h
|
||||||
|
src/mainobject.cpp
|
||||||
|
src/apgetconnected.h
|
||||||
|
src/apgetconnected.cpp
|
||||||
|
src/pipewire.h
|
||||||
|
src/pipewire.cpp
|
||||||
|
|
||||||
src/ui/mainwindow.h
|
src/ui/mainwindow.h
|
||||||
src/ui/mainwindow.cpp
|
src/ui/mainwindow.cpp
|
||||||
|
|
@ -148,7 +173,7 @@ target_sources(SHinterface
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add UI files
|
# Add UI files
|
||||||
target_sources(SHinterface
|
target_sources(smartvos
|
||||||
PRIVATE
|
PRIVATE
|
||||||
src/ui/mainwindow.ui
|
src/ui/mainwindow.ui
|
||||||
src/ui/itemwidget.ui
|
src/ui/itemwidget.ui
|
||||||
|
|
@ -169,13 +194,14 @@ target_sources(SHinterface
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add resource file
|
# Add resource file
|
||||||
target_sources(SHinterface
|
target_sources(smartvos
|
||||||
PRIVATE
|
PRIVATE
|
||||||
resources.qrc
|
resources.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
# Link libraries
|
# Link libraries - link to static library plus UI-specific dependencies
|
||||||
target_link_libraries(SHinterface
|
target_link_libraries(smartvos
|
||||||
|
smartvos_core
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
|
|
@ -190,3 +216,16 @@ target_link_libraries(SHinterface
|
||||||
|
|
||||||
# Add include paths
|
# Add include paths
|
||||||
include_directories(${PIPEWIRE_INCLUDE_DIRS} ${LIBNL3_INCLUDE_DIRS})
|
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})")
|
||||||
|
|
|
||||||
10
smartvos.desktop
Normal file
10
smartvos.desktop
Normal file
|
|
@ -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
|
||||||
|
|
@ -60,6 +60,13 @@ void ItemData::storeWithChanges(QJsonObject& json, const ItemFieldChanges& chang
|
||||||
json["Value"] = static_cast<double>(value_);
|
json["Value"] = static_cast<double>(value_);
|
||||||
if(changes.groupName)
|
if(changes.groupName)
|
||||||
json["GroupName"] = 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)
|
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();
|
groupName_ = json["GroupName"].toString();
|
||||||
changes.groupName = true;
|
changes.groupName = true;
|
||||||
}
|
}
|
||||||
|
if(json.contains("ValueType"))
|
||||||
|
{
|
||||||
|
type_ = static_cast<item_value_type_t>(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<uint32_t>(json["ItemId"].toDouble(0));
|
itemId_ = static_cast<uint32_t>(json["ItemId"].toDouble(0));
|
||||||
}
|
}
|
||||||
return changes;
|
return changes;
|
||||||
|
|
@ -120,6 +140,8 @@ bool ItemData::hasChanged(const ItemData& other, const ItemFieldChanges& changes
|
||||||
return true;
|
return true;
|
||||||
if(changes.actors)
|
if(changes.actors)
|
||||||
return true;
|
return true;
|
||||||
|
if(changes.valueNames && other.getValueNames() != getValueNames())
|
||||||
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,6 +170,33 @@ void ItemData::setGroupName(QString groupName)
|
||||||
groupName_ = groupName;
|
groupName_ = groupName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<QString> ItemData::getValueNames() const
|
||||||
|
{
|
||||||
|
return valueNames_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemData::setValueNames(std::vector<QString> 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<int>(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ItemData::indexToValueName(int index) const
|
||||||
|
{
|
||||||
|
if(index >= 0 && static_cast<size_t>(index) < valueNames_.size())
|
||||||
|
return valueNames_[index];
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
//item
|
//item
|
||||||
|
|
||||||
Item::Item(uint32_t itemIdIn, QString name, uint8_t value, QObject *parent): QObject(parent), ItemData (itemIdIn, name,
|
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();
|
itemId_ = other.id();
|
||||||
hidden_ = other.isHidden();
|
hidden_ = other.isHidden();
|
||||||
groupName_ = other.getGroupName();
|
groupName_ = other.getGroupName();
|
||||||
|
valueNames_ = other.getValueNames();
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,6 +298,8 @@ void Item::requestUpdate(ItemUpdateRequest update)
|
||||||
for(std::shared_ptr<Actor>& actor : update.newActors)
|
for(std::shared_ptr<Actor>& actor : update.newActors)
|
||||||
addActor(actor);
|
addActor(actor);
|
||||||
}
|
}
|
||||||
|
if(update.changes.valueNames)
|
||||||
|
valueNames_ = update.payload.getValueNames();
|
||||||
update.payload = *this;
|
update.payload = *this;
|
||||||
updated(update);
|
updated(update);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ public:
|
||||||
item_value_type_t getValueType();
|
item_value_type_t getValueType();
|
||||||
QString getGroupName() const;
|
QString getGroupName() const;
|
||||||
void setGroupName(QString groupName);
|
void setGroupName(QString groupName);
|
||||||
|
std::vector<QString> getValueNames() const;
|
||||||
|
void setValueNames(std::vector<QString> valueNames);
|
||||||
|
int valueNameToIndex(const QString& name) const;
|
||||||
|
QString indexToValueName(int index) const;
|
||||||
void storeWithChanges(QJsonObject& json, const ItemFieldChanges& changes);
|
void storeWithChanges(QJsonObject& json, const ItemFieldChanges& changes);
|
||||||
ItemFieldChanges loadWithChanges(const QJsonObject& json, const bool preserve = false);
|
ItemFieldChanges loadWithChanges(const QJsonObject& json, const bool preserve = false);
|
||||||
virtual QString getName() const;
|
virtual QString getName() const;
|
||||||
|
|
@ -130,6 +134,7 @@ struct ItemFieldChanges
|
||||||
bool type :1;
|
bool type :1;
|
||||||
bool groupName :1;
|
bool groupName :1;
|
||||||
bool actors :1;
|
bool actors :1;
|
||||||
|
bool valueNames :1;
|
||||||
ItemFieldChanges(bool defaultVal = false)
|
ItemFieldChanges(bool defaultVal = false)
|
||||||
{
|
{
|
||||||
name = defaultVal;
|
name = defaultVal;
|
||||||
|
|
@ -138,10 +143,11 @@ struct ItemFieldChanges
|
||||||
type = defaultVal;
|
type = defaultVal;
|
||||||
groupName = defaultVal;
|
groupName = defaultVal;
|
||||||
actors = false;
|
actors = false;
|
||||||
|
valueNames = defaultVal;
|
||||||
}
|
}
|
||||||
inline bool isNone() const
|
inline bool isNone() const
|
||||||
{
|
{
|
||||||
return !name && !value && !hidden && !type && !groupName && !actors;
|
return !name && !value && !hidden && !type && !groupName && !actors && !valueNames;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ void ItemLoaderSource::refresh()
|
||||||
request.type = ITEM_UPDATE_LOADED;
|
request.type = ITEM_UPDATE_LOADED;
|
||||||
request.payload = newItem;
|
request.payload = newItem;
|
||||||
request.changes = ItemFieldChanges(true);
|
request.changes = ItemFieldChanges(true);
|
||||||
|
request.changes.value = false;
|
||||||
itemAddRequests.push_back(request);
|
itemAddRequests.push_back(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
#include "mqttitem.h"
|
#include "mqttitem.h"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QtMqtt/QMqttClient>
|
#include <QtMqtt/QMqttClient>
|
||||||
|
|
||||||
#include "mqttclient.h"
|
#include "mqttclient.h"
|
||||||
|
#include "programmode.h"
|
||||||
|
|
||||||
MqttItem::MqttItem(QString name, uint8_t value, QObject *parent)
|
MqttItem::MqttItem(QString name, uint8_t value, QObject *parent)
|
||||||
: Item(0, name, value, parent),
|
: Item(0, name, value, parent),
|
||||||
|
|
@ -15,8 +17,9 @@ MqttItem::MqttItem(QString name, uint8_t value, QObject *parent)
|
||||||
{
|
{
|
||||||
hashId();
|
hashId();
|
||||||
std::shared_ptr<MqttClient> workClient = client.lock();
|
std::shared_ptr<MqttClient> workClient = client.lock();
|
||||||
assert(workClient);
|
assert(workClient || programMode == PROGRAM_MODE_UI_ONLY);
|
||||||
|
|
||||||
|
if(workClient)
|
||||||
connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged);
|
connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,16 +27,35 @@ MqttItem::~MqttItem()
|
||||||
{
|
{
|
||||||
qDebug()<<__func__;
|
qDebug()<<__func__;
|
||||||
std::shared_ptr<MqttClient> workClient = client.lock();
|
std::shared_ptr<MqttClient> workClient = client.lock();
|
||||||
if(!workClient || topic_.isEmpty() || !subscription)
|
if(!workClient)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if(subscription)
|
||||||
|
{
|
||||||
|
disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
|
||||||
workClient->unsubscribe(subscription);
|
workClient->unsubscribe(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(devicesSubscription)
|
||||||
|
{
|
||||||
|
disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
|
||||||
|
workClient->unsubscribe(devicesSubscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MqttItem::onClientStateChanged(QMqttClient::ClientState state)
|
void MqttItem::onClientStateChanged(QMqttClient::ClientState state)
|
||||||
{
|
{
|
||||||
if(state == QMqttClient::Connected)
|
if(state == QMqttClient::Connected)
|
||||||
|
{
|
||||||
refreshSubscription();
|
refreshSubscription();
|
||||||
|
// Subscribe to bridge/devices to get exposes
|
||||||
|
std::shared_ptr<MqttClient> workClient = client.lock();
|
||||||
|
if(workClient && !exposeLoaded_)
|
||||||
|
{
|
||||||
|
devicesSubscription = workClient->subscribe(workClient->getBaseTopic() + "/bridge/devices");
|
||||||
|
connect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MqttItem::refreshSubscription()
|
void MqttItem::refreshSubscription()
|
||||||
|
|
@ -55,6 +77,101 @@ void MqttItem::refreshSubscription()
|
||||||
connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
|
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<MqttClient> 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)
|
void MqttItem::onMessageReceived(const QMqttMessage& message)
|
||||||
{
|
{
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(message.payload());
|
QJsonDocument doc = QJsonDocument::fromJson(message.payload());
|
||||||
|
|
@ -63,13 +180,32 @@ void MqttItem::onMessageReceived(const QMqttMessage& message)
|
||||||
QJsonObject obj = doc.object();
|
QJsonObject obj = doc.object();
|
||||||
if(obj.contains(getValueKey()))
|
if(obj.contains(getValueKey()))
|
||||||
{
|
{
|
||||||
QString value = obj[getValueKey()].toString();
|
QJsonValue value = obj[getValueKey()];
|
||||||
ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND);
|
ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND);
|
||||||
req.changes.value = true;
|
req.changes.value = true;
|
||||||
if(value == getValueOn())
|
|
||||||
|
if(getValueType() == ITEM_VALUE_UINT)
|
||||||
|
{
|
||||||
|
// Numeric value
|
||||||
|
req.payload.setValueData(value.toInt(0));
|
||||||
|
}
|
||||||
|
else if(getValueType() == ITEM_VALUE_ENUM)
|
||||||
|
{
|
||||||
|
// Enum value - find index
|
||||||
|
QString strValue = value.toString();
|
||||||
|
int index = valueNameToIndex(strValue);
|
||||||
|
if(index >= 0)
|
||||||
|
req.payload.setValueData(index);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Binary value
|
||||||
|
QString strValue = value.toString();
|
||||||
|
if(strValue == getValueOn() || strValue == "ON" || strValue == "true")
|
||||||
req.payload.setValueData(true);
|
req.payload.setValueData(true);
|
||||||
else
|
else
|
||||||
req.payload.setValueData(false);
|
req.payload.setValueData(false);
|
||||||
|
}
|
||||||
requestUpdate(req);
|
requestUpdate(req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +240,59 @@ void MqttItem::setValueOff(const QString& valueOff)
|
||||||
valueOff_ = 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<QString> valueNames;
|
||||||
|
for(const QJsonValue& v : values)
|
||||||
|
valueNames.push_back(v.toString());
|
||||||
|
setValueNames(valueNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
hashId();
|
||||||
|
}
|
||||||
|
|
||||||
QString MqttItem::getTopic() const
|
QString MqttItem::getTopic() const
|
||||||
{
|
{
|
||||||
return topic_;
|
return topic_;
|
||||||
|
|
@ -124,6 +313,26 @@ QString MqttItem::getValueOff() const
|
||||||
return valueOff_;
|
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)
|
void MqttItem::store(QJsonObject& json)
|
||||||
{
|
{
|
||||||
Item::store(json);
|
Item::store(json);
|
||||||
|
|
@ -132,6 +341,9 @@ void MqttItem::store(QJsonObject& json)
|
||||||
json["ValueKey"] = valueKey_;
|
json["ValueKey"] = valueKey_;
|
||||||
json["ValueOn"] = valueOn_;
|
json["ValueOn"] = valueOn_;
|
||||||
json["ValueOff"] = valueOff_;
|
json["ValueOff"] = valueOff_;
|
||||||
|
json["ValueMin"] = valueMin_;
|
||||||
|
json["ValueMax"] = valueMax_;
|
||||||
|
json["ValueStep"] = valueStep_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MqttItem::load(const QJsonObject& json, const bool preserve)
|
void MqttItem::load(const QJsonObject& json, const bool preserve)
|
||||||
|
|
@ -141,6 +353,10 @@ void MqttItem::load(const QJsonObject& json, const bool preserve)
|
||||||
valueKey_ = json["ValueKey"].toString("state");
|
valueKey_ = json["ValueKey"].toString("state");
|
||||||
valueOn_ = json["ValueOn"].toString("ON");
|
valueOn_ = json["ValueOn"].toString("ON");
|
||||||
valueOff_ = json["ValueOff"].toString("OFF");
|
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();
|
hashId();
|
||||||
refreshSubscription();
|
refreshSubscription();
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +370,18 @@ void MqttItem::enactValue(uint8_t value)
|
||||||
QString fullTopic = workClient->getBaseTopic() + "/" + topic_ + "/set";
|
QString fullTopic = workClient->getBaseTopic() + "/" + topic_ + "/set";
|
||||||
QJsonObject payload;
|
QJsonObject payload;
|
||||||
|
|
||||||
|
if(getValueType() == ITEM_VALUE_UINT)
|
||||||
|
{
|
||||||
|
payload[valueKey_] = static_cast<int>(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);
|
QJsonDocument doc(payload);
|
||||||
QByteArray data = doc.toJson(QJsonDocument::Compact);
|
QByteArray data = doc.toJson(QJsonDocument::Compact);
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,22 @@ public:
|
||||||
private:
|
private:
|
||||||
QString topic_;
|
QString topic_;
|
||||||
QString valueKey_;
|
QString valueKey_;
|
||||||
QString valueOn_;
|
QString valueOn_ = "ON";
|
||||||
QString valueOff_;
|
QString valueOff_ = "OFF";
|
||||||
|
int valueMin_ = 0;
|
||||||
|
int valueMax_ = 255;
|
||||||
|
int valueStep_ = 1;
|
||||||
|
bool exposeLoaded_ = false;
|
||||||
|
|
||||||
MqttClient::Subscription* subscription = nullptr;
|
MqttClient::Subscription* subscription = nullptr;
|
||||||
|
MqttClient::Subscription* devicesSubscription = nullptr;
|
||||||
|
|
||||||
void hashId();
|
void hashId();
|
||||||
void refreshSubscription();
|
void refreshSubscription();
|
||||||
void onMessageReceived(const QMqttMessage& message);
|
void onMessageReceived(const QMqttMessage& message);
|
||||||
void onClientStateChanged(QMqttClient::ClientState state);
|
void onClientStateChanged(QMqttClient::ClientState state);
|
||||||
|
void onDevicesMessageReceived(const QMqttMessage& message);
|
||||||
|
void loadExposeFromDevice(const QJsonObject& device);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MqttItem(QString name = "MqttItem",
|
explicit MqttItem(QString name = "MqttItem",
|
||||||
|
|
@ -36,12 +43,22 @@ public:
|
||||||
void setBaseTopic(const QString& baseTopic);
|
void setBaseTopic(const QString& baseTopic);
|
||||||
void setValueOn(const QString& valueOn);
|
void setValueOn(const QString& valueOn);
|
||||||
void setValueOff(const QString& valueOff);
|
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 getTopic() const;
|
||||||
QString getValueKey() const;
|
QString getValueKey() const;
|
||||||
QString getBaseTopic() const;
|
|
||||||
QString getValueOn() const;
|
QString getValueOn() const;
|
||||||
QString getValueOff() const;
|
QString getValueOff() const;
|
||||||
|
int getValueMin() const;
|
||||||
|
int getValueMax() const;
|
||||||
|
int getValueStep() const;
|
||||||
|
bool getExposeLoaded() const;
|
||||||
|
|
||||||
virtual void store(QJsonObject& json) override;
|
virtual void store(QJsonObject& json) override;
|
||||||
virtual void load(const QJsonObject& json, const bool preserve = false) override;
|
virtual void load(const QJsonObject& json, const bool preserve = false) override;
|
||||||
|
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
#include "mqttitemsource.h"
|
|
||||||
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
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<MqttItem>(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<ItemAddRequest> 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_;
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
#ifndef MQTTITEMSOURCE_H
|
|
||||||
#define MQTTITEMSOURCE_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QtMqtt/QMqttClient>
|
|
||||||
#include <vector>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#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
|
|
||||||
|
|
@ -16,11 +16,12 @@ void MqttClient::start(const QJsonObject& settings)
|
||||||
|
|
||||||
client->setHostname(settings["Host"].toString("127.0.0.1"));
|
client->setHostname(settings["Host"].toString("127.0.0.1"));
|
||||||
client->setPort(settings["Port"].toInt(1883));
|
client->setPort(settings["Port"].toInt(1883));
|
||||||
|
client->setClientId(settings["ClientId"].toString("smartvos"));
|
||||||
if(settings.contains("User"))
|
if(settings.contains("User"))
|
||||||
client->setUsername(settings["User"].toString());
|
client->setUsername(settings["User"].toString());
|
||||||
if(settings.contains("Password"))
|
if(settings.contains("Password"))
|
||||||
client->setPassword(settings["Password"].toString());
|
client->setPassword(settings["Password"].toString());
|
||||||
client->setProtocolVersion(QMqttClient::MQTT_5_0);
|
client->setProtocolVersion(QMqttClient::MQTT_3_1);
|
||||||
|
|
||||||
client->connectToHost();
|
client->connectToHost();
|
||||||
}
|
}
|
||||||
|
|
@ -83,13 +84,17 @@ void MqttClient::unsubscribe(MqttClient::Subscription* subscription)
|
||||||
|
|
||||||
void MqttClient::unsubscribe(QString topic)
|
void MqttClient::unsubscribe(QString topic)
|
||||||
{
|
{
|
||||||
assert(!subscriptions.contains(topic));
|
|
||||||
MqttClient::Subscription* sub = subscriptions[topic];
|
MqttClient::Subscription* sub = subscriptions[topic];
|
||||||
|
if(!sub)
|
||||||
|
{
|
||||||
|
qWarning()<<"MqttClient: Trying to unsubscribe from unkown topic:"<<topic;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(--sub->ref > 0)
|
if(--sub->ref > 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
qDebug()<<"MqttClient: unsubscibeing"<<sub->subscription->topic();
|
qDebug()<<"MqttClient: unsubscibeing"<<sub->subscription->topic().filter();
|
||||||
client->unsubscribe(sub->subscription->topic());
|
client->unsubscribe(sub->subscription->topic());
|
||||||
subscriptions.erase(topic);
|
subscriptions.erase(topic);
|
||||||
delete sub;
|
delete sub;
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ void MqttSensorSource::onClientStateChanged(QMqttClient::ClientState state)
|
||||||
{
|
{
|
||||||
for(SensorSubscription& sensor : sensors)
|
for(SensorSubscription& sensor : sensors)
|
||||||
{
|
{
|
||||||
qDebug()<<"MQTT subscribeing to"<<client->getBaseTopic() + "/" + sensor.topic;
|
|
||||||
sensor.subscription = client->subscribe(client->getBaseTopic() + "/" + sensor.topic);
|
sensor.subscription = client->subscribe(client->getBaseTopic() + "/" + sensor.topic);
|
||||||
connect(sensor.subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived);
|
connect(sensor.subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived);
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +57,7 @@ void MqttSensorSource::onClientStateChanged(QMqttClient::ClientState state)
|
||||||
{
|
{
|
||||||
if(sensor.subscription)
|
if(sensor.subscription)
|
||||||
{
|
{
|
||||||
client->unsubscribe(sensor.topic);
|
client->unsubscribe(client->getBaseTopic() + "/" + sensor.topic);
|
||||||
sensor.subscription = nullptr;
|
sensor.subscription = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,6 +147,30 @@ void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
|
||||||
sensor.field = obj["voc"].toDouble(0);
|
sensor.field = obj["voc"].toDouble(0);
|
||||||
stateChanged(sensor);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,6 +190,6 @@ void MqttSensorSource::store(QJsonObject& json)
|
||||||
MqttSensorSource::~MqttSensorSource()
|
MqttSensorSource::~MqttSensorSource()
|
||||||
{
|
{
|
||||||
for(SensorSubscription& sub : sensors)
|
for(SensorSubscription& sub : sensors)
|
||||||
client->unsubscribe(sub.topic);
|
client->unsubscribe(client->getBaseTopic() + "/" + sub.topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ public:
|
||||||
TYPE_FORMALDEHYD,
|
TYPE_FORMALDEHYD,
|
||||||
TYPE_PM25,
|
TYPE_PM25,
|
||||||
TYPE_TOTAL_VOC,
|
TYPE_TOTAL_VOC,
|
||||||
|
TYPE_ENERGY_USE,
|
||||||
|
TYPE_POWER,
|
||||||
|
TYPE_VOLTAGE,
|
||||||
TYPE_LOWBATTERY = 128,
|
TYPE_LOWBATTERY = 128,
|
||||||
TYPE_SHUTDOWN_IMMINENT = 251,
|
TYPE_SHUTDOWN_IMMINENT = 251,
|
||||||
TYPE_OCUPANCY,
|
TYPE_OCUPANCY,
|
||||||
|
|
@ -145,6 +148,12 @@ public:
|
||||||
return "ppb";
|
return "ppb";
|
||||||
case TYPE_SUN_ALTITUDE:
|
case TYPE_SUN_ALTITUDE:
|
||||||
return "°";
|
return "°";
|
||||||
|
case TYPE_POWER:
|
||||||
|
return "W";
|
||||||
|
case TYPE_ENERGY_USE:
|
||||||
|
return "kWh";
|
||||||
|
case TYPE_VOLTAGE:
|
||||||
|
return "V";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ void Server::itemUpdated(ItemUpdateRequest update)
|
||||||
{
|
{
|
||||||
QJsonArray items;
|
QJsonArray items;
|
||||||
QJsonObject itemjson;
|
QJsonObject itemjson;
|
||||||
update.payload.store(itemjson);
|
update.payload.storeWithChanges(itemjson, update.changes);
|
||||||
items.append(itemjson);
|
items.append(itemjson);
|
||||||
QJsonObject json = createMessage("ItemUpdate", items);
|
QJsonObject json = createMessage("ItemUpdate", items);
|
||||||
json["FullList"] = false;
|
json["FullList"] = false;
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ void TcpClient::itemUpdated(ItemUpdateRequest update)
|
||||||
{
|
{
|
||||||
QJsonArray items;
|
QJsonArray items;
|
||||||
QJsonObject itemjson;
|
QJsonObject itemjson;
|
||||||
update.payload.store(itemjson);
|
update.payload.storeWithChanges(itemjson, update.changes);
|
||||||
items.append(itemjson);
|
items.append(itemjson);
|
||||||
QJsonObject json = createMessage("ItemUpdate", items);
|
QJsonObject json = createMessage("ItemUpdate", items);
|
||||||
json["FullList"] = false;
|
json["FullList"] = false;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "itemscrollbox.h"
|
#include "itemscrollbox.h"
|
||||||
#include "ui_relayscrollbox.h"
|
#include "ui_relayscrollbox.h"
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
|
#include <QScroller>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
|
|
||||||
ItemScrollBox::ItemScrollBox(QWidget *parent) :
|
ItemScrollBox::ItemScrollBox(QWidget *parent) :
|
||||||
|
|
@ -131,7 +132,7 @@ void ItemScrollBox::onItemUpdate(const ItemUpdateRequest& update)
|
||||||
{
|
{
|
||||||
if(widget->controles(update.payload))
|
if(widget->controles(update.payload))
|
||||||
{
|
{
|
||||||
qDebug()<<"ItemUpdate with group change";
|
qDebug()<<"ItemUpdate with group change for item"<<update.payload.getName()<<"type"<<update.type;
|
||||||
std::weak_ptr<Item> item = widget->getItem();
|
std::weak_ptr<Item> item = widget->getItem();
|
||||||
removeItemFromTabs(update.payload);
|
removeItemFromTabs(update.payload);
|
||||||
addItemToTabs(item);
|
addItemToTabs(item);
|
||||||
|
|
@ -148,6 +149,7 @@ void ItemScrollBox::ensureTabExists(const QString& groupName)
|
||||||
tab.scroller->setWidgetResizable(true);
|
tab.scroller->setWidgetResizable(true);
|
||||||
tab.scroller->setFrameShape(QFrame::NoFrame);
|
tab.scroller->setFrameShape(QFrame::NoFrame);
|
||||||
tab.scroller->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
tab.scroller->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
QScroller::grabGesture(tab.scroller->viewport(), QScroller::LeftMouseButtonGesture);
|
||||||
|
|
||||||
tab.content = new QWidget(tab.scroller);
|
tab.content = new QWidget(tab.scroller);
|
||||||
QVBoxLayout* scrollLayout = new QVBoxLayout(tab.content);
|
QVBoxLayout* scrollLayout = new QVBoxLayout(tab.content);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "ui_itemwidget.h"
|
#include "ui_itemwidget.h"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
#include <QComboBox>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
#include "itemsettingsdialog.h"
|
#include "itemsettingsdialog.h"
|
||||||
|
|
@ -20,15 +21,28 @@ ItemWidget::ItemWidget(std::weak_ptr<Item> item, bool noGroupEdit, QWidget *pare
|
||||||
{
|
{
|
||||||
ui->horizontalSpacer->changeSize(0,0);
|
ui->horizontalSpacer->changeSize(0,0);
|
||||||
ui->checkBox->hide();
|
ui->checkBox->hide();
|
||||||
|
ui->comboBox->hide();
|
||||||
}
|
}
|
||||||
else if(workingItem->getValueType() == ITEM_VALUE_NO_VALUE)
|
else if(workingItem->getValueType() == ITEM_VALUE_NO_VALUE)
|
||||||
{
|
{
|
||||||
ui->checkBox->hide();
|
ui->checkBox->hide();
|
||||||
ui->slider->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
|
else
|
||||||
{
|
{
|
||||||
ui->slider->hide();
|
ui->slider->hide();
|
||||||
|
ui->comboBox->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui->checkBox->setChecked(workingItem->getValue());
|
ui->checkBox->setChecked(workingItem->getValue());
|
||||||
|
|
@ -37,6 +51,8 @@ ItemWidget::ItemWidget(std::weak_ptr<Item> item, bool noGroupEdit, QWidget *pare
|
||||||
|
|
||||||
if(workingItem->getValueType() == ITEM_VALUE_UINT)
|
if(workingItem->getValueType() == ITEM_VALUE_UINT)
|
||||||
connect(ui->slider, &QSlider::valueChanged, this, &ItemWidget::moveToValue);
|
connect(ui->slider, &QSlider::valueChanged, this, &ItemWidget::moveToValue);
|
||||||
|
else if(workingItem->getValueType() == ITEM_VALUE_ENUM)
|
||||||
|
connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ItemWidget::moveToValue);
|
||||||
else
|
else
|
||||||
connect(ui->checkBox, &QCheckBox::toggled, this, &ItemWidget::moveToState);
|
connect(ui->checkBox, &QCheckBox::toggled, this, &ItemWidget::moveToState);
|
||||||
connect(ui->pushButton, &QPushButton::clicked, this, &ItemWidget::showSettingsDialog);
|
connect(ui->pushButton, &QPushButton::clicked, this, &ItemWidget::showSettingsDialog);
|
||||||
|
|
@ -84,6 +100,7 @@ void ItemWidget::disable()
|
||||||
ui->checkBox->setEnabled(false);
|
ui->checkBox->setEnabled(false);
|
||||||
ui->label->setEnabled(false);
|
ui->label->setEnabled(false);
|
||||||
ui->slider->setEnabled(false);
|
ui->slider->setEnabled(false);
|
||||||
|
ui->comboBox->setEnabled(false);
|
||||||
ui->pushButton_Remove->setEnabled(false);
|
ui->pushButton_Remove->setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +132,16 @@ std::weak_ptr<Item> ItemWidget::getItem()
|
||||||
|
|
||||||
void ItemWidget::onItemUpdated(ItemUpdateRequest update)
|
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());
|
stateChanged(update.payload.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,6 +153,9 @@ void ItemWidget::stateChanged(int state)
|
||||||
ui->checkBox->blockSignals(true);
|
ui->checkBox->blockSignals(true);
|
||||||
ui->checkBox->setChecked(state);
|
ui->checkBox->setChecked(state);
|
||||||
ui->checkBox->blockSignals(false);
|
ui->checkBox->blockSignals(false);
|
||||||
|
ui->comboBox->blockSignals(true);
|
||||||
|
ui->comboBox->setCurrentIndex(state);
|
||||||
|
ui->comboBox->blockSignals(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemWidget::~ItemWidget()
|
ItemWidget::~ItemWidget()
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,16 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="comboBox">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="checkBox">
|
<widget class="QCheckBox" name="checkBox">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
|
|
||||||
|
|
@ -6,152 +6,19 @@ enable_testing()
|
||||||
# Find Qt packages for tests
|
# Find Qt packages for tests
|
||||||
find_package(Qt6 COMPONENTS Core Gui Widgets Multimedia Test REQUIRED)
|
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
|
# Add test executables - link to static library instead of compiling sources
|
||||||
set(COMMON_TEST_SOURCES
|
add_executable(test_item unit/items/test_item.cpp)
|
||||||
../src/items/item.h
|
add_executable(test_sensor unit/sensors/test_sensor.cpp)
|
||||||
../src/items/item.cpp
|
add_executable(test_actor unit/actors/test_actor.cpp)
|
||||||
../src/sensors/sensor.h
|
add_executable(test_itemstore unit/items/test_itemstore.cpp)
|
||||||
../src/sensors/sensor.cpp
|
add_executable(test_itemloadersource unit/items/test_itemloadersource.cpp)
|
||||||
../src/programmode.h
|
add_executable(test_mqttitem unit/items/test_mqttitem.cpp)
|
||||||
../src/programmode.cpp
|
add_executable(test_tcp unit/service/test_tcp.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 - compile all needed sources into each test
|
# Link all tests to static library
|
||||||
add_executable(test_item unit/items/test_item.cpp ${COMMON_TEST_SOURCES})
|
foreach(test test_item test_sensor test_actor test_itemstore test_itemloadersource test_mqttitem test_tcp)
|
||||||
add_executable(test_sensor unit/sensors/test_sensor.cpp ${COMMON_TEST_SOURCES})
|
target_link_libraries(${test}
|
||||||
add_executable(test_actor unit/actors/test_actor.cpp ${COMMON_TEST_SOURCES})
|
smartvos_core
|
||||||
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::Core
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
|
|
@ -162,11 +29,12 @@ target_link_libraries(test_tcp
|
||||||
Qt6::Test
|
Qt6::Test
|
||||||
)
|
)
|
||||||
|
|
||||||
# Include paths for source files
|
# Include paths - the static library already has the correct include paths
|
||||||
target_include_directories(test_tcp PRIVATE
|
target_include_directories(${test} PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../src
|
${CMAKE_CURRENT_SOURCE_DIR}/../src
|
||||||
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
|
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
|
||||||
)
|
)
|
||||||
|
endforeach()
|
||||||
|
|
||||||
# Add tests to CTest
|
# Add tests to CTest
|
||||||
add_test(NAME test_item COMMAND test_item)
|
add_test(NAME test_item COMMAND test_item)
|
||||||
|
|
@ -174,4 +42,5 @@ add_test(NAME test_sensor COMMAND test_sensor)
|
||||||
add_test(NAME test_actor COMMAND test_actor)
|
add_test(NAME test_actor COMMAND test_actor)
|
||||||
add_test(NAME test_itemstore COMMAND test_itemstore)
|
add_test(NAME test_itemstore COMMAND test_itemstore)
|
||||||
add_test(NAME test_itemloadersource COMMAND test_itemloadersource)
|
add_test(NAME test_itemloadersource COMMAND test_itemloadersource)
|
||||||
|
add_test(NAME test_mqttitem COMMAND test_mqttitem)
|
||||||
add_test(NAME test_tcp COMMAND test_tcp)
|
add_test(NAME test_tcp COMMAND test_tcp)
|
||||||
306
tests/unit/items/test_mqttitem.cpp
Normal file
306
tests/unit/items/test_mqttitem.cpp
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
#include <QtTest/QtTest>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
#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> mqttClient;
|
||||||
|
if (!host.isEmpty()) {
|
||||||
|
qDebug() << "MQTT configured:" << host << port;
|
||||||
|
mqttClient = std::make_shared<MqttClient>();
|
||||||
|
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<QString> 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<QString> 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"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue