Add the ability to add mqtt sensors at runtime

This commit is contained in:
Carl Philipp Klemm 2026-04-26 13:50:06 +02:00
parent 36171a221a
commit 25b9f87285
11 changed files with 208 additions and 0 deletions

View file

@ -22,6 +22,11 @@ void MainObject::refresh()
globalItems.refresh(); globalItems.refresh();
} }
void MainObject::addSensor(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload)
{
// Default implementation does nothing - derived classes override
}
QJsonObject MainObject::getJsonObjectFromDisk(const QString& filename, bool* error) QJsonObject MainObject::getJsonObjectFromDisk(const QString& filename, bool* error)
{ {
QFile file; QFile file;
@ -89,6 +94,8 @@ PrimaryMainObject::PrimaryMainObject(QIODevice* microDevice, const QString& sett
connect(&sunSensorSource, &SunSensorSource::stateChanged, &globalSensors, &SensorStore::sensorGotState); connect(&sunSensorSource, &SunSensorSource::stateChanged, &globalSensors, &SensorStore::sensorGotState);
connect(&micro, &Microcontroller::gotSensorState, &globalSensors, &SensorStore::sensorGotState); connect(&micro, &Microcontroller::gotSensorState, &globalSensors, &SensorStore::sensorGotState);
connect(&mqttSensorSource, &MqttSensorSource::stateChanged, &globalSensors, &SensorStore::sensorGotState); connect(&mqttSensorSource, &MqttSensorSource::stateChanged, &globalSensors, &SensorStore::sensorGotState);
connect(tcpServer, &TcpServer::sensorAdded, &mqttSensorSource, &MqttSensorSource::onSensorAdded);
connect(webServer, &WebSocketServer::sensorAdded, &mqttSensorSource, &MqttSensorSource::onSensorAdded);
globalItems.registerItemSource(&fixedItems); globalItems.registerItemSource(&fixedItems);
globalItems.registerItemSource(tcpServer); globalItems.registerItemSource(tcpServer);
@ -174,3 +181,8 @@ SecondaryMainObject::~SecondaryMainObject()
{ {
} }
void SecondaryMainObject::addSensor(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload)
{
tcpClient->addSensor(sensor, backend, payload);
}

View file

@ -33,6 +33,7 @@ public:
public slots: public slots:
void refresh(); void refresh();
virtual void addSensor(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload = {});
}; };
class PrimaryMainObject : public MainObject class PrimaryMainObject : public MainObject
@ -74,6 +75,7 @@ public:
public: public:
explicit SecondaryMainObject(QString host, int port, QObject *parent = nullptr); explicit SecondaryMainObject(QString host, int port, QObject *parent = nullptr);
~SecondaryMainObject(); ~SecondaryMainObject();
void addSensor(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload = {}) override;
}; };

View file

@ -31,6 +31,25 @@ void MqttSensorSource::start(std::shared_ptr<MqttClient> client, const QJsonObje
} }
} }
void MqttSensorSource::addSensor(const QString& topic, const QString& name)
{
if(!client)
return;
SensorSubscription sensor;
sensor.topic = topic;
sensor.name = name;
sensor.id = qHash(client->getBaseTopic() + "/" + topic);
sensors.push_back(sensor);
// Subscribe if already connected
if(client->getClient()->state() == QMqttClient::ClientState::Connected)
{
sensor.subscription = client->subscribe(client->getBaseTopic() + "/" + topic);
connect(sensor.subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived);
}
}
MqttSensorSource::SensorSubscription& MqttSensorSource::findSubscription(const QString& topic) MqttSensorSource::SensorSubscription& MqttSensorSource::findSubscription(const QString& topic)
{ {
for(SensorSubscription& sensor : sensors) for(SensorSubscription& sensor : sensors)
@ -194,3 +213,16 @@ MqttSensorSource::~MqttSensorSource()
client->unsubscribe(client->getBaseTopic() + "/" + sub.topic); client->unsubscribe(client->getBaseTopic() + "/" + sub.topic);
} }
void MqttSensorSource::onSensorAdded(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload)
{
if(backend != Sensor::BACKEND_MQTT)
return;
QString topic = payload["Topic"].toString();
QString name = payload["Name"].toString();
if(topic.isEmpty())
return;
addSensor(topic, name);
}

View file

@ -31,10 +31,14 @@ private slots:
void onClientStateChanged(QMqttClient::ClientState state); void onClientStateChanged(QMqttClient::ClientState state);
void onMessageReceived(const QMqttMessage& message); void onMessageReceived(const QMqttMessage& message);
public slots:
void onSensorAdded(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload);
public: public:
explicit MqttSensorSource(QObject *parent = nullptr); explicit MqttSensorSource(QObject *parent = nullptr);
~MqttSensorSource(); ~MqttSensorSource();
void start(std::shared_ptr<MqttClient> client, const QJsonObject& settings); void start(std::shared_ptr<MqttClient> client, const QJsonObject& settings);
void addSensor(const QString& topic, const QString& name);
void store(QJsonObject& json); void store(QJsonObject& json);
signals: signals:

View file

@ -33,6 +33,12 @@ public:
TYPE_DUMMY, TYPE_DUMMY,
} sensor_type_t; } sensor_type_t;
typedef enum {
BACKEND_MICROCONTROLLER = 0,
BACKEND_MQTT,
BACKEND_SUN,
} sensor_backend_type_t;
sensor_type_t type; sensor_type_t type;
uint64_t id; uint64_t id;
float field; float field;

View file

@ -31,6 +31,17 @@ void Service::sensorEvent(Sensor sensor, sensor_update_type_t type)
void Service::itemUpdated(ItemUpdateRequest update) {} void Service::itemUpdated(ItemUpdateRequest update) {}
void Service::addSensor(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload)
{
QJsonObject sensorjson;
sensor.store(sensorjson);
QJsonObject json = createMessage("AddSensor", QJsonArray());
json["Sensor"] = sensorjson;
json["Backend"] = static_cast<int>(backend);
json["Payload"] = payload;
sendJson(json);
}
void Service::refresh() void Service::refresh()
{ {
sendJson(createMessage("GetSensors", QJsonArray())); sendJson(createMessage("GetSensors", QJsonArray()));
@ -88,4 +99,12 @@ void Service::processIncomeingJson(const QByteArray& jsonbytes)
gotSensor(sensor, SENSOR_UPDATE_REMOTE); gotSensor(sensor, SENSOR_UPDATE_REMOTE);
} }
} }
else if(type == "AddSensor")
{
QJsonObject sensorjson = json["Sensor"].toObject();
Sensor sensor(sensorjson);
Sensor::sensor_backend_type_t backend = static_cast<Sensor::sensor_backend_type_t>(json["Backend"].toInt(0));
QJsonObject payload = json["Payload"].toObject();
emit sensorAdded(sensor, backend, payload);
}
} }

View file

@ -21,11 +21,13 @@ protected:
signals: signals:
void gotSensor(Sensor sensor, sensor_update_type_t type = SENSOR_UPDATE_BACKEND); void gotSensor(Sensor sensor, sensor_update_type_t type = SENSOR_UPDATE_BACKEND);
void sensorAdded(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload);
public slots: public slots:
void sensorEvent(Sensor sensor, sensor_update_type_t type); void sensorEvent(Sensor sensor, sensor_update_type_t type);
virtual void itemUpdated(ItemUpdateRequest update); virtual void itemUpdated(ItemUpdateRequest update);
virtual void refresh() override; virtual void refresh() override;
virtual void addSensor(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload = {});
public: public:
Service(QObject* parent = nullptr); Service(QObject* parent = nullptr);

View file

@ -1,6 +1,7 @@
#include "mainwindow.h" #include "mainwindow.h"
#include <QMessageBox> #include <QMessageBox>
#include <QInputDialog>
#include "ui_mainwindow.h" #include "ui_mainwindow.h"
#include "itemscrollbox.h" #include "itemscrollbox.h"
@ -9,10 +10,12 @@
#include "mainobject.h" #include "mainobject.h"
#include "programmode.h" #include "programmode.h"
#include "items/poweritem.h" #include "items/poweritem.h"
#include "sensors/mqttsensorsource.h"
MainWindow::MainWindow(MainObject * const mainObject, QWidget *parent) : MainWindow::MainWindow(MainObject * const mainObject, QWidget *parent) :
QMainWindow(parent), QMainWindow(parent),
ui(new Ui::MainWindow), ui(new Ui::MainWindow),
mainObject(mainObject),
colorChooser(this) colorChooser(this)
{ {
ui->setupUi(this); ui->setupUi(this);
@ -45,6 +48,7 @@ MainWindow::MainWindow(MainObject * const mainObject, QWidget *parent) :
ui->button_color->hide(); ui->button_color->hide();
connect(ui->pushButton_addItem, &QPushButton::clicked, this, &MainWindow::showItemCreationDialog); connect(ui->pushButton_addItem, &QPushButton::clicked, this, &MainWindow::showItemCreationDialog);
connect(ui->pushButton_addSensor, &QPushButton::clicked, this, &MainWindow::showSensorCreationDialog);
connect(ui->relayList, &ItemScrollBox::deleteRequest, &globalItems, &ItemStore::removeItem); connect(ui->relayList, &ItemScrollBox::deleteRequest, &globalItems, &ItemStore::removeItem);
connect(ui->checkBox_sensorsShowHidden, &QCheckBox::clicked, ui->sensorListView, &SensorListWidget::setShowHidden); connect(ui->checkBox_sensorsShowHidden, &QCheckBox::clicked, ui->sensorListView, &SensorListWidget::setShowHidden);
@ -90,6 +94,35 @@ void MainWindow::showItemCreationDialog()
} }
} }
void MainWindow::showSensorCreationDialog()
{
bool ok;
QString topic = QInputDialog::getText(this, "Add MQTT Sensor", "Topic:", QLineEdit::Normal, "", &ok);
if(!ok || topic.isEmpty())
return;
QString name = QInputDialog::getText(this, "Add MQTT Sensor", "Name:", QLineEdit::Normal, topic, &ok);
if(!ok)
return;
Sensor sensor(Sensor::TYPE_DUMMY, 0, 0, name);
QJsonObject payload;
payload["Topic"] = topic;
payload["Name"] = name;
PrimaryMainObject* primaryMain = dynamic_cast<PrimaryMainObject*>(mainObject);
if(primaryMain)
{
// Primary mode: add directly to mqttSensorSource
primaryMain->mqttSensorSource.addSensor(topic, name);
}
else
{
// Secondary mode: send via TCP to primary
mainObject->addSensor(sensor, Sensor::BACKEND_MQTT, payload);
}
}
void MainWindow::changeHeaderLableText(QString string) void MainWindow::changeHeaderLableText(QString string)
{ {
if(string.size() > 28) if(string.size() > 28)

View file

@ -26,6 +26,7 @@ public:
private: private:
Ui::MainWindow *ui; Ui::MainWindow *ui;
MainObject* const mainObject;
QColorDialog colorChooser; QColorDialog colorChooser;
@ -40,6 +41,7 @@ private slots:
//RGB //RGB
void showPowerItemDialog(); void showPowerItemDialog();
void showItemCreationDialog(); void showItemCreationDialog();
void showSensorCreationDialog();
public slots: public slots:

View file

@ -230,6 +230,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="pushButton_addSensor">
<property name="text">
<string>Add Sensor</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="button_quit"> <widget class="QPushButton" name="button_quit">
<property name="sizePolicy"> <property name="sizePolicy">

View file

@ -10,6 +10,7 @@
#include "service/server.h" #include "service/server.h"
#include "service/service.h" #include "service/service.h"
#include "items/item.h" #include "items/item.h"
#include "sensors/sensor.h"
class TestTcp : public QObject class TestTcp : public QObject
{ {
@ -332,6 +333,94 @@ private slots:
QVERIFY(size == jsonData.size()); QVERIFY(size == jsonData.size());
} }
void testAddSensorMessageFormat()
{
// Test creating an AddSensor message format
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0, "temp_sensor");
QJsonObject sensorJson;
sensor.store(sensorJson);
QJsonObject payload;
payload["Topic"] = "home/temperature";
payload["Name"] = "Living Room";
// Manually create the message (since createMessage is protected)
QJsonObject message;
message["MessageType"] = "AddSensor";
message["Data"] = QJsonArray();
message["Sensor"] = sensorJson;
message["Backend"] = static_cast<int>(Sensor::BACKEND_MQTT);
message["Payload"] = payload;
QVERIFY(message["MessageType"].toString() == "AddSensor");
QVERIFY(message["Sensor"].toObject()["Name"].toString() == "temp_sensor");
QVERIFY(message["Backend"].toInt() == Sensor::BACKEND_MQTT);
QVERIFY(message["Payload"].toObject()["Topic"].toString() == "home/temperature");
}
void testSensorBackendTypeEnum()
{
// Test that the backend type enum values are correct
QVERIFY(Sensor::BACKEND_MICROCONTROLLER == 0);
QVERIFY(Sensor::BACKEND_MQTT == 1);
QVERIFY(Sensor::BACKEND_SUN == 2);
}
void testServiceAddSensor()
{
// Test that Service::addSensor can be called without crashing
// and creates proper JSON message format
TcpServer server;
bool result = server.launch(QHostAddress::LocalHost, 0);
QVERIFY(result);
// Create a sensor and payload
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0, "temp_sensor");
QJsonObject payload;
payload["Topic"] = "home/temperature";
payload["Name"] = "Living Room";
// Call addSensor - this should not crash
// The actual sending to clients depends on network timing
server.addSensor(sensor, Sensor::BACKEND_MQTT, payload);
// Test passed if no crash occurred
QVERIFY(true);
}
void testServiceProcessAddSensorMessage()
{
// Test processing an incoming AddSensor message
// We test the JSON parsing directly without network
QJsonObject sensorJson;
sensorJson["SensorType"] = static_cast<int>(Sensor::TYPE_TEMPERATURE);
sensorJson["Id"] = 1;
sensorJson["Field"] = 25.0;
sensorJson["Name"] = "temp_sensor";
QJsonObject payload;
payload["Topic"] = "home/temperature";
payload["Name"] = "Living Room";
QJsonObject message;
message["MessageType"] = "AddSensor";
message["Data"] = QJsonArray();
message["Sensor"] = sensorJson;
message["Backend"] = static_cast<int>(Sensor::BACKEND_MQTT);
message["Payload"] = payload;
// Test that the message can be parsed correctly
QJsonDocument doc(message);
QVERIFY(doc.isObject());
QJsonObject parsed = doc.object();
QVERIFY(parsed["MessageType"].toString() == "AddSensor");
QVERIFY(parsed["Backend"].toInt() == Sensor::BACKEND_MQTT);
QVERIFY(parsed["Payload"].toObject()["Topic"].toString() == "home/temperature");
QVERIFY(parsed["Sensor"].toObject()["Name"].toString() == "temp_sensor");
}
void cleanupTestCase() void cleanupTestCase()
{ {
// Cleanup after all tests // Cleanup after all tests