Compare commits

..

11 commits

19 changed files with 864 additions and 497 deletions

View file

@ -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
View 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

View file

@ -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);
} }

View file

@ -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;
} }
}; };

View file

@ -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);
} }
} }

View file

@ -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);

View file

@ -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;

View file

@ -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_;
}

View file

@ -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

View file

@ -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;

View file

@ -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);
} }

View file

@ -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 "";
} }

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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()

View file

@ -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">

View file

@ -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
@ -160,13 +27,14 @@ target_link_libraries(test_tcp
Qt6::WebSockets Qt6::WebSockets
Qt6::Mqtt Qt6::Mqtt
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)

View 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"