Add Mqtt item

This commit is contained in:
Carl Philipp Klemm 2026-04-12 18:06:19 +02:00
parent 0fd50eb227
commit eb60f85604
19 changed files with 901 additions and 48 deletions

View file

@ -32,7 +32,11 @@ add_subdirectory(tests)
# Create executable # Create executable
add_executable(SHinterface add_executable(SHinterface
src/sensors/mqttsensorsource.h src/sensors/mqttsensorsource.cpp) src/sensors/mqttsensorsource.h src/sensors/mqttsensorsource.cpp
src/items/mqttitem.h src/items/mqttitem.cpp
src/mqttclient.h src/mqttclient.cpp
)
# Add sources to executable # Add sources to executable
target_sources(SHinterface target_sources(SHinterface
@ -139,6 +143,8 @@ target_sources(SHinterface
src/ui/itemsettingswidgets/relayitemsettingswidget.cpp src/ui/itemsettingswidgets/relayitemsettingswidget.cpp
src/ui/itemsettingswidgets/systemitemsettingswidget.h src/ui/itemsettingswidgets/systemitemsettingswidget.h
src/ui/itemsettingswidgets/systemitemsettingswidget.cpp src/ui/itemsettingswidgets/systemitemsettingswidget.cpp
src/ui/itemsettingswidgets/mqttitemsettingswidget.h
src/ui/itemsettingswidgets/mqttitemsettingswidget.cpp
) )
# Add UI files # Add UI files
@ -159,6 +165,7 @@ target_sources(SHinterface
src/ui/itemsettingswidgets/messageitemsettingswidget.ui src/ui/itemsettingswidgets/messageitemsettingswidget.ui
src/ui/itemsettingswidgets/relayitemsettingswidget.ui src/ui/itemsettingswidgets/relayitemsettingswidget.ui
src/ui/itemsettingswidgets/systemitemsettingswidget.ui src/ui/itemsettingswidgets/systemitemsettingswidget.ui
src/ui/itemsettingswidgets/mqttitemsettingswidget.ui
) )
# Add resource file # Add resource file

View file

@ -10,6 +10,7 @@
#include "auxitem.h" #include "auxitem.h"
#include "poweritem.h" #include "poweritem.h"
#include "rgbitem.h" #include "rgbitem.h"
#include "mqttitem.h"
#include <QJsonArray> #include <QJsonArray>
@ -338,6 +339,8 @@ std::shared_ptr<Item> Item::loadItem(const QJsonObject& json)
newItem = std::shared_ptr<PowerItem>(new PowerItem); newItem = std::shared_ptr<PowerItem>(new PowerItem);
else if(json["Type"].toString("") == "Rgb") else if(json["Type"].toString("") == "Rgb")
newItem = std::shared_ptr<RgbItem>(new RgbItem); newItem = std::shared_ptr<RgbItem>(new RgbItem);
else if(json["Type"].toString("") == "Mqtt")
newItem = std::shared_ptr<MqttItem>(new MqttItem);
else else
qWarning()<<"Unable to load unkown item type: "<<json["Type"].toString(); qWarning()<<"Unable to load unkown item type: "<<json["Type"].toString();
if(newItem) if(newItem)

164
src/items/mqttitem.cpp Normal file
View file

@ -0,0 +1,164 @@
#include "mqttitem.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <QtMqtt/QMqttClient>
#include "mqttclient.h"
MqttItem::MqttItem(QString name, uint8_t value, QObject *parent)
: Item(0, name, value, parent),
topic_(""),
valueKey_("state"),
valueOn_("ON"),
valueOff_("OFF")
{
hashId();
std::shared_ptr<MqttClient> workClient = client.lock();
assert(workClient);
connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged);
}
MqttItem::~MqttItem()
{
qDebug()<<__func__;
std::shared_ptr<MqttClient> workClient = client.lock();
if(!workClient || topic_.isEmpty() || !subscription)
return;
workClient->unsubscribe(subscription);
}
void MqttItem::onClientStateChanged(QMqttClient::ClientState state)
{
if(state == QMqttClient::Connected)
refreshSubscription();
}
void MqttItem::refreshSubscription()
{
std::shared_ptr<MqttClient> workClient = client.lock();
if(!workClient || topic_.isEmpty())
return;
if(workClient->getClient()->state() != QMqttClient::Connected)
return;
if(subscription)
{
disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
workClient->unsubscribe(subscription);
}
subscription = workClient->subscribe(workClient->getBaseTopic() + "/" + getTopic());
connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
}
void MqttItem::onMessageReceived(const QMqttMessage& message)
{
QJsonDocument doc = QJsonDocument::fromJson(message.payload());
if(doc.isObject())
{
QJsonObject obj = doc.object();
if(obj.contains(getValueKey()))
{
QString value = obj[getValueKey()].toString();
ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND);
req.changes.value = true;
if(value == getValueOn())
req.payload.setValueData(true);
else
req.payload.setValueData(false);
requestUpdate(req);
}
}
}
void MqttItem::hashId()
{
QString hashString = topic_ + "/" + valueKey_;
itemId_ = qHash(hashString.toLatin1());
}
void MqttItem::setTopic(const QString& topic)
{
topic_ = topic;
hashId();
refreshSubscription();
}
void MqttItem::setValueKey(const QString& valueKey)
{
valueKey_ = valueKey;
hashId();
}
void MqttItem::setValueOn(const QString& valueOn)
{
valueOn_ = valueOn;
}
void MqttItem::setValueOff(const QString& valueOff)
{
valueOff_ = valueOff;
}
QString MqttItem::getTopic() const
{
return topic_;
}
QString MqttItem::getValueKey() const
{
return valueKey_;
}
QString MqttItem::getValueOn() const
{
return valueOn_;
}
QString MqttItem::getValueOff() const
{
return valueOff_;
}
void MqttItem::store(QJsonObject& json)
{
Item::store(json);
json["Type"] = "Mqtt";
json["Topic"] = topic_;
json["ValueKey"] = valueKey_;
json["ValueOn"] = valueOn_;
json["ValueOff"] = valueOff_;
}
void MqttItem::load(const QJsonObject& json, const bool preserve)
{
Item::load(json, preserve);
topic_ = json["Topic"].toString();
valueKey_ = json["ValueKey"].toString("state");
valueOn_ = json["ValueOn"].toString("ON");
valueOff_ = json["ValueOff"].toString("OFF");
hashId();
refreshSubscription();
}
void MqttItem::enactValue(uint8_t value)
{
std::shared_ptr<MqttClient> workClient = client.lock();
if(!workClient || topic_.isEmpty())
return;
QString fullTopic = workClient->getBaseTopic() + "/" + topic_ + "/set";
QJsonObject payload;
payload[valueKey_] = value ? valueOn_ : valueOff_;
QJsonDocument doc(payload);
QByteArray data = doc.toJson(QJsonDocument::Compact);
qDebug() << "MqttItem publishing to" << fullTopic << ":" << data;
workClient->getClient()->publish(fullTopic, data);
}

53
src/items/mqttitem.h Normal file
View file

@ -0,0 +1,53 @@
#ifndef MQTTITEM_H
#define MQTTITEM_H
#include "item.h"
#include "mqttclient.h"
class QString;
class MqttItem : public Item
{
Q_OBJECT
public:
inline static std::weak_ptr<MqttClient> client;
private:
QString topic_;
QString valueKey_;
QString valueOn_;
QString valueOff_;
MqttClient::Subscription* subscription = nullptr;
void hashId();
void refreshSubscription();
void onMessageReceived(const QMqttMessage& message);
void onClientStateChanged(QMqttClient::ClientState state);
public:
explicit MqttItem(QString name = "MqttItem",
uint8_t value = 0,
QObject *parent = nullptr);
virtual ~MqttItem() override;
void setTopic(const QString& topic);
void setValueKey(const QString& valueKey);
void setBaseTopic(const QString& baseTopic);
void setValueOn(const QString& valueOn);
void setValueOff(const QString& valueOff);
QString getTopic() const;
QString getValueKey() const;
QString getBaseTopic() const;
QString getValueOn() const;
QString getValueOff() const;
virtual void store(QJsonObject& json) override;
virtual void load(const QJsonObject& json, const bool preserve = false) override;
protected:
virtual void enactValue(uint8_t value) override;
};
#endif // MQTTITEM_H

View file

@ -0,0 +1,205 @@
#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

@ -0,0 +1,34 @@
#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

@ -4,6 +4,8 @@
#include<QJsonArray> #include<QJsonArray>
#include<QMessageBox> #include<QMessageBox>
#include "mqttclient.h"
#include "items/mqttitem.h"
#include "items/itemstore.h" #include "items/itemstore.h"
MainObject::MainObject(QObject *parent) : MainObject::MainObject(QObject *parent) :
@ -74,9 +76,12 @@ PrimaryMainObject::PrimaryMainObject(QIODevice* microDevice, const QString& sett
micro(microDevice), micro(microDevice),
tcpServer(new TcpServer(this)), tcpServer(new TcpServer(this)),
webServer(new WebSocketServer("shinterface", this)), webServer(new WebSocketServer("shinterface", this)),
mqttClient(new MqttClient),
sunSensorSource(49.824972, 8.702194), sunSensorSource(49.824972, 8.702194),
fixedItems(&micro) fixedItems(&micro)
{ {
MqttItem::client = mqttClient;
//connect sensors subsystem //connect sensors subsystem
connect(&globalSensors, &SensorStore::sensorChangedState, tcpServer, &TcpServer::sensorEvent); connect(&globalSensors, &SensorStore::sensorChangedState, tcpServer, &TcpServer::sensorEvent);
connect(tcpServer, &TcpServer::gotSensor, &globalSensors, &SensorStore::sensorGotState); connect(tcpServer, &TcpServer::gotSensor, &globalSensors, &SensorStore::sensorGotState);
@ -98,7 +103,8 @@ PrimaryMainObject::PrimaryMainObject(QIODevice* microDevice, const QString& sett
loadFromDisk(settingsPath); loadFromDisk(settingsPath);
QJsonObject mqttJson = settings["Mqtt"].toObject(); QJsonObject mqttJson = settings["Mqtt"].toObject();
mqttSensorSource.start(mqttJson); mqttClient->start(mqttJson);
mqttSensorSource.start(mqttClient, mqttJson);
tcpServer->launch(QHostAddress(host), port); tcpServer->launch(QHostAddress(host), port);
webServer->launch(QHostAddress(host), port+1); webServer->launch(QHostAddress(host), port+1);
@ -115,6 +121,7 @@ void PrimaryMainObject::store(QJsonObject &json)
{ {
globalItems.store(json); globalItems.store(json);
QJsonObject mqttJson = json["Mqtt"].toObject(); QJsonObject mqttJson = json["Mqtt"].toObject();
mqttClient->store(mqttJson);
mqttSensorSource.store(mqttJson); mqttSensorSource.store(mqttJson);
json["Mqtt"] = mqttJson; json["Mqtt"] = mqttJson;
} }

View file

@ -46,6 +46,7 @@ public:
Microcontroller micro; Microcontroller micro;
TcpServer* tcpServer; TcpServer* tcpServer;
WebSocketServer* webServer; WebSocketServer* webServer;
std::shared_ptr<MqttClient> mqttClient;
//sensors //sensors
SunSensorSource sunSensorSource; SunSensorSource sunSensorSource;

110
src/mqttclient.cpp Normal file
View file

@ -0,0 +1,110 @@
#include "mqttclient.h"
MqttClient::MqttClient():
client(new QMqttClient)
{
}
void MqttClient::start(const QJsonObject& settings)
{
baseTopicName = settings["BaseTopic"].toString("zigbee2mqtt");
QMqttClient* cl = client.get();
connect(cl, &QMqttClient::stateChanged, this, &MqttClient::onClientStateChanged);
connect(cl, &QMqttClient::errorChanged, this, &MqttClient::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();
}
void MqttClient::onClientError(QMqttClient::ClientError error)
{
qWarning()<<"MQTT Client error:"<<error;
}
void MqttClient::onClientStateChanged(QMqttClient::ClientState state)
{
if(state == QMqttClient::ClientState::Connected)
qInfo()<<"Connected to MQTT broker at "<<client->hostname()<<client->port();
else if (state == QMqttClient::ClientState::Disconnected)
qWarning()<<"Lost connection to MQTT broker";
else if(state == QMqttClient::ClientState::Connecting)
qInfo()<<"Connecting to MQTT broker at "<<client->hostname()<<client->port();
}
void MqttClient::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();
}
std::shared_ptr<QMqttClient> MqttClient::getClient()
{
return client;
}
MqttClient::Subscription* MqttClient::subscribe(QString topic)
{
if(subscriptions.contains(topic))
{
MqttClient::Subscription* sub = subscriptions[topic];
++sub->ref;
return sub;
}
else
{
qDebug()<<"MqttClient: subscibeing to"<<topic;
MqttClient::Subscription* sub = new Subscription;
sub->subscription = client->subscribe(topic);
sub->ref = 1;
subscriptions.insert({topic, sub});
return sub;
}
}
void MqttClient::unsubscribe(MqttClient::Subscription* subscription)
{
QString topic = subscription->subscription->topic().filter();
unsubscribe(topic);
}
void MqttClient::unsubscribe(QString topic)
{
assert(!subscriptions.contains(topic));
MqttClient::Subscription* sub = subscriptions[topic];
if(--sub->ref > 0)
return;
qDebug()<<"MqttClient: unsubscibeing"<<sub->subscription->topic();
client->unsubscribe(sub->subscription->topic());
subscriptions.erase(topic);
delete sub;
}
QString MqttClient::getBaseTopic()
{
return baseTopicName;
}
MqttClient::~MqttClient()
{
for(const std::pair<QString, Subscription*> sub : subscriptions)
{
qWarning()<<sub.first<<"not unregistered at exit!";
client->unsubscribe(sub.second->subscription->topic());
}
}

40
src/mqttclient.h Normal file
View file

@ -0,0 +1,40 @@
#ifndef MQTTCLIENT_H
#define MQTTCLIENT_H
#include <QMqttClient>
#include <QObject>
#include <QJsonObject>
#include <map>
class MqttClient: public QObject
{
Q_OBJECT
public:
struct Subscription
{
int ref;
QMqttSubscription* subscription;
};
private:
QString baseTopicName;
std::shared_ptr<QMqttClient> client;
std::map<QString, Subscription*> subscriptions;
private slots:
void onClientStateChanged(QMqttClient::ClientState state);
void onClientError(QMqttClient::ClientError error);
public:
explicit MqttClient();
~MqttClient();
void start(const QJsonObject& settings);
void store(QJsonObject& json);
std::shared_ptr<QMqttClient> getClient();
Subscription* subscribe(QString topic);
void unsubscribe(Subscription* subscription);
void unsubscribe(QString topic);
QString getBaseTopic();
};
#endif // MQTTCLIENT_H

View file

@ -7,22 +7,11 @@ MqttSensorSource::MqttSensorSource(QObject *parent)
{ {
} }
void MqttSensorSource::start(const QJsonObject& settings) void MqttSensorSource::start(std::shared_ptr<MqttClient> client, const QJsonObject& settings)
{ {
baseTopicName = settings["BaseTopic"].toString("zigbee2mqtt"); this->client = client;
connect(&client, &QMqttClient::stateChanged, this, &MqttSensorSource::onClientStateChanged); connect(client->getClient().get(), &QMqttClient::stateChanged, this, &MqttSensorSource::onClientStateChanged);
connect(&client, &QMqttClient::errorChanged, this, &MqttSensorSource::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 sensorsArray = settings["Sensors"].toArray(); QJsonArray sensorsArray = settings["Sensors"].toArray();
@ -37,21 +26,16 @@ void MqttSensorSource::start(const QJsonObject& settings)
sensor.name = sensor.topic; sensor.name = sensor.topic;
else else
sensor.name = sensorObject["Name"].toString(); sensor.name = sensorObject["Name"].toString();
sensor.id = qHash(baseTopicName + "/" + sensor.topic); sensor.id = qHash(client->getBaseTopic() + "/" + sensor.topic);
sensors.push_back(sensor); sensors.push_back(sensor);
} }
} }
void MqttSensorSource::onClientError(QMqttClient::ClientError error)
{
qWarning()<<"MQTT Client error:"<<error;
}
MqttSensorSource::SensorSubscription& MqttSensorSource::findSubscription(const QString& topic) MqttSensorSource::SensorSubscription& MqttSensorSource::findSubscription(const QString& topic)
{ {
for(SensorSubscription& sensor : sensors) for(SensorSubscription& sensor : sensors)
{ {
if(baseTopicName + "/" + sensor.topic == topic) if(client->getBaseTopic() + "/" + sensor.topic == topic)
return sensor; return sensor;
} }
assert(false); assert(false);
@ -61,30 +45,24 @@ void MqttSensorSource::onClientStateChanged(QMqttClient::ClientState state)
{ {
if(state == QMqttClient::ClientState::Connected) if(state == QMqttClient::ClientState::Connected)
{ {
qInfo()<<"Connected to MQTT broker at "<<client.hostname()<<client.port();
for(SensorSubscription& sensor : sensors) for(SensorSubscription& sensor : sensors)
{ {
qDebug()<<"MQTT subscribeing to"<<baseTopicName + "/" + sensor.topic; qDebug()<<"MQTT subscribeing to"<<client->getBaseTopic() + "/" + sensor.topic;
sensor.subscription = client.subscribe(baseTopicName + "/" + sensor.topic); sensor.subscription = client->subscribe(client->getBaseTopic() + "/" + sensor.topic);
connect(sensor.subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived); connect(sensor.subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived);
} }
} }
else if (state == QMqttClient::ClientState::Disconnected) else if (state == QMqttClient::ClientState::Disconnected)
{ {
qWarning()<<"Lost connection to MQTT broker";
for(SensorSubscription& sensor : sensors) for(SensorSubscription& sensor : sensors)
{ {
if(sensor.subscription) if(sensor.subscription)
{ {
client.unsubscribe(sensor.topic); client->unsubscribe(sensor.topic);
sensor.subscription = nullptr; sensor.subscription = nullptr;
} }
} }
} }
else if(state == QMqttClient::ClientState::Connecting)
{
qInfo()<<"Connecting to MQTT broker at "<<client.hostname()<<client.port();
}
} }
void MqttSensorSource::onMessageReceived(const QMqttMessage& message) void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
@ -107,6 +85,14 @@ void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
stateChanged(sensor); stateChanged(sensor);
} }
if(obj.contains("local_temperature"))
{
sensor.name = baseName + " Temperature";
sensor.type = Sensor::TYPE_TEMPERATURE;
sensor.field = obj["local_temperature"].toDouble(0);
stateChanged(sensor);
}
if(obj.contains("humidity")) if(obj.contains("humidity"))
{ {
sensor.name = baseName + " Humidity"; sensor.name = baseName + " Humidity";
@ -167,13 +153,6 @@ void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
void MqttSensorSource::store(QJsonObject& json) void MqttSensorSource::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 sensorsArray; QJsonArray sensorsArray;
for(const SensorSubscription& sensor : sensors) for(const SensorSubscription& sensor : sensors)
{ {
@ -185,3 +164,9 @@ void MqttSensorSource::store(QJsonObject& json)
json["Sensors"] = sensorsArray; json["Sensors"] = sensorsArray;
} }
MqttSensorSource::~MqttSensorSource()
{
for(SensorSubscription& sub : sensors)
client->unsubscribe(sub.topic);
}

View file

@ -7,6 +7,7 @@
#include <vector> #include <vector>
#include "sensor.h" #include "sensor.h"
#include "mqttclient.h"
class MqttSensorSource : public QObject class MqttSensorSource : public QObject
{ {
@ -17,12 +18,11 @@ class MqttSensorSource : public QObject
uint64_t id; uint64_t id;
QString topic; QString topic;
QString name; QString name;
QMqttSubscription* subscription = nullptr; MqttClient::Subscription* subscription = nullptr;
}; };
QString baseTopicName;
std::vector<SensorSubscription> sensors; std::vector<SensorSubscription> sensors;
QMqttClient client; std::shared_ptr<MqttClient> client;
private: private:
SensorSubscription& findSubscription(const QString& topic); SensorSubscription& findSubscription(const QString& topic);
@ -30,11 +30,11 @@ private:
private slots: private slots:
void onClientStateChanged(QMqttClient::ClientState state); void onClientStateChanged(QMqttClient::ClientState state);
void onMessageReceived(const QMqttMessage& message); void onMessageReceived(const QMqttMessage& message);
void onClientError(QMqttClient::ClientError error);
public: public:
explicit MqttSensorSource(QObject *parent = nullptr); explicit MqttSensorSource(QObject *parent = nullptr);
void start(const QJsonObject& settings); ~MqttSensorSource();
void start(std::shared_ptr<MqttClient> client, const QJsonObject& settings);
void store(QJsonObject& json); void store(QJsonObject& json);
signals: signals:

View file

@ -3,6 +3,8 @@
#include "itemsettingswidgets/messageitemsettingswidget.h" #include "itemsettingswidgets/messageitemsettingswidget.h"
#include "itemsettingswidgets/systemitemsettingswidget.h" #include "itemsettingswidgets/systemitemsettingswidget.h"
#include "itemsettingswidgets/mqttitemsettingswidget.h"
#include "items/mqttitem.h"
ItemCreationDialog::ItemCreationDialog(QWidget *parent) : ItemCreationDialog::ItemCreationDialog(QWidget *parent) :
QDialog(parent), QDialog(parent),
@ -41,6 +43,13 @@ void ItemCreationDialog::itemTypeChanged(const QString& type)
widget = new SystemItemSettingsWidget(systemItem, this); widget = new SystemItemSettingsWidget(systemItem, this);
ui->verticalLayout->addWidget(widget); ui->verticalLayout->addWidget(widget);
} }
if(type == "Mqtt")
{
std::shared_ptr<MqttItem> mqttItem(new MqttItem);
item = mqttItem;
widget = new MqttItemSettingsWidget(mqttItem, this);
ui->verticalLayout->addWidget(widget);
}
} }
void ItemCreationDialog::itemNameChanged(const QString& name) void ItemCreationDialog::itemNameChanged(const QString& name)

View file

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>400</width> <width>400</width>
<height>140</height> <height>274</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -39,6 +39,11 @@
<string>System</string> <string>System</string>
</property> </property>
</item> </item>
<item>
<property name="text">
<string>Mqtt</string>
</property>
</item>
</widget> </widget>
</item> </item>
</layout> </layout>
@ -64,13 +69,26 @@
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout"/> <layout class="QVBoxLayout" name="verticalLayout"/>
</item> </item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="standardButtons"> <property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> <set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property> </property>
</widget> </widget>
</item> </item>

View file

@ -12,6 +12,8 @@
#include "itemsettingswidgets/messageitemsettingswidget.h" #include "itemsettingswidgets/messageitemsettingswidget.h"
#include "itemsettingswidgets/systemitemsettingswidget.h" #include "itemsettingswidgets/systemitemsettingswidget.h"
#include "itemsettingswidgets/relayitemsettingswidget.h" #include "itemsettingswidgets/relayitemsettingswidget.h"
#include "itemsettingswidgets/mqttitemsettingswidget.h"
#include "../items/mqttitem.h"
#include<memory> #include<memory>
ItemSettingsDialog::ItemSettingsDialog(std::shared_ptr<Item> item, bool noGroup, QWidget *parent) : ItemSettingsDialog::ItemSettingsDialog(std::shared_ptr<Item> item, bool noGroup, QWidget *parent) :
@ -47,6 +49,10 @@ ItemSettingsDialog::ItemSettingsDialog(std::shared_ptr<Item> item, bool noGroup,
{ {
itemSpecificWidget_ = new SystemItemSettingsWidget(sysItem); itemSpecificWidget_ = new SystemItemSettingsWidget(sysItem);
} }
else if(std::shared_ptr<MqttItem> mqttItem = std::dynamic_pointer_cast<MqttItem>(item_))
{
itemSpecificWidget_ = new MqttItemSettingsWidget(mqttItem);
}
if(itemSpecificWidget_) if(itemSpecificWidget_)
{ {

View file

@ -0,0 +1,62 @@
#include "mqttitemsettingswidget.h"
#include "ui_mqttitemsettingswidget.h"
#include <QDebug>
MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent) :
QWidget(parent),
item_(item),
ui(new Ui::MqttItemSettingsWidget)
{
ui->setupUi(this);
if(auto workingItem = item_.lock())
{
ui->lineEdit_topic->setText(workingItem->getTopic());
ui->lineEdit_valueKey->setText(workingItem->getValueKey());
ui->lineEdit_valueOn->setText(workingItem->getValueOn());
ui->lineEdit_valueOff->setText(workingItem->getValueOff());
}
connect(ui->lineEdit_topic, &QLineEdit::textChanged, this, &MqttItemSettingsWidget::setTopic);
connect(ui->lineEdit_valueKey, &QLineEdit::textChanged, this, &MqttItemSettingsWidget::setValueKey);
connect(ui->lineEdit_valueOn, &QLineEdit::textChanged, this, &MqttItemSettingsWidget::setValueOn);
connect(ui->lineEdit_valueOff, &QLineEdit::textChanged, this, &MqttItemSettingsWidget::setValueOff);
}
void MqttItemSettingsWidget::setTopic(const QString& topic)
{
if(auto workingItem = item_.lock())
{
workingItem->setTopic(topic);
}
}
void MqttItemSettingsWidget::setValueKey(const QString& valueKey)
{
if(auto workingItem = item_.lock())
{
workingItem->setValueKey(valueKey);
}
}
void MqttItemSettingsWidget::setValueOn(const QString& valueOn)
{
if(auto workingItem = item_.lock())
{
workingItem->setValueOn(valueOn);
}
}
void MqttItemSettingsWidget::setValueOff(const QString& valueOff)
{
if(auto workingItem = item_.lock())
{
workingItem->setValueOff(valueOff);
}
}
MqttItemSettingsWidget::~MqttItemSettingsWidget()
{
delete ui;
}

View file

@ -0,0 +1,32 @@
#ifndef MQTTITEMSETTINGSWIDGET_H
#define MQTTITEMSETTINGSWIDGET_H
#include <QWidget>
#include <memory>
#include "../../items/mqttitem.h"
namespace Ui
{
class MqttItemSettingsWidget;
}
class MqttItemSettingsWidget : public QWidget
{
Q_OBJECT
std::weak_ptr<MqttItem> item_;
private slots:
void setTopic(const QString& topic);
void setValueKey(const QString& valueKey);
void setValueOn(const QString& valueOn);
void setValueOff(const QString& valueOff);
public:
explicit MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent = nullptr);
~MqttItemSettingsWidget();
private:
Ui::MqttItemSettingsWidget *ui;
};
#endif // MQTTITEMSETTINGSWIDGET_H

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MqttItemSettingsWidget</class>
<widget class="QWidget" name="MqttItemSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>216</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_topic">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_topic">
<property name="text">
<string>Topic:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_topic">
<property name="placeholderText">
<string>e.g., kitchen/light</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_valueKey">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_valueKey">
<property name="text">
<string>Value Key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_valueKey">
<property name="text">
<string>state</string>
</property>
<property name="placeholderText">
<string>e.g., state, brightness</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_valueOn">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_valueOn">
<property name="text">
<string>Value On:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_valueOn">
<property name="text">
<string>ON</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_valueOff">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_valueOff">
<property name="text">
<string>Value Off:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_valueOff">
<property name="text">
<string>OFF</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -48,6 +48,9 @@ set(COMMON_TEST_SOURCES
../src/items/itemstore.cpp ../src/items/itemstore.cpp
../src/items/itemloadersource.h ../src/items/itemloadersource.h
../src/items/itemloadersource.cpp ../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 # Add test executables - compile all needed sources into each test
@ -73,6 +76,7 @@ target_link_libraries(test_item
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
Qt6::Multimedia Qt6::Multimedia
Qt6::Mqtt
Qt6::Test Qt6::Test
) )
@ -88,6 +92,7 @@ target_link_libraries(test_sensor
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
Qt6::Multimedia Qt6::Multimedia
Qt6::Mqtt
Qt6::Test Qt6::Test
) )
@ -103,6 +108,7 @@ target_link_libraries(test_actor
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
Qt6::Multimedia Qt6::Multimedia
Qt6::Mqtt
Qt6::Test Qt6::Test
) )
@ -118,6 +124,7 @@ target_link_libraries(test_itemstore
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
Qt6::Multimedia Qt6::Multimedia
Qt6::Mqtt
Qt6::Test Qt6::Test
) )
@ -133,6 +140,7 @@ target_link_libraries(test_itemloadersource
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
Qt6::Multimedia Qt6::Multimedia
Qt6::Mqtt
Qt6::Test Qt6::Test
) )
@ -150,6 +158,7 @@ target_link_libraries(test_tcp
Qt6::Multimedia Qt6::Multimedia
Qt6::Network Qt6::Network
Qt6::WebSockets Qt6::WebSockets
Qt6::Mqtt
Qt6::Test Qt6::Test
) )