Add support for mqtt sensors

This commit is contained in:
Carl Philipp Klemm 2026-03-27 17:47:09 +01:00
parent d6c8d799e3
commit e3b6d5c3a6
7 changed files with 251 additions and 5 deletions

View file

@ -10,7 +10,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_compile_options(-Wall)
# Find Qt packages
find_package(Qt6 COMPONENTS Core Gui Widgets Network Multimedia SerialPort REQUIRED)
find_package(Qt6 COMPONENTS Core Gui Widgets Network Multimedia SerialPort Mqtt REQUIRED)
# Find dependencies using pkg-config
find_package(PkgConfig REQUIRED)
@ -25,7 +25,8 @@ set(CMAKE_AUTOUIC ON)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
# Create executable
add_executable(SHinterface)
add_executable(SHinterface
src/sensors/mqttsensorsource.h src/sensors/mqttsensorsource.cpp)
# Add sources to executable
target_sources(SHinterface
@ -159,6 +160,7 @@ target_link_libraries(SHinterface
Qt6::Network
Qt6::Multimedia
Qt6::SerialPort
Qt6::Mqtt
${PIPEWIRE_LIBRARIES}
${LIBNL3_LIBRARIES}
)

View file

@ -81,6 +81,7 @@ PrimaryMainObject::PrimaryMainObject(QIODevice* microDevice, const QString& sett
connect(tcpServer, &TcpServer::gotSensor, &globalSensors, &SensorStore::sensorGotState);
connect(&sunSensorSource, &SunSensorSource::stateChanged, &globalSensors, &SensorStore::sensorGotState);
connect(&micro, &Microcontroller::gotSensorState, &globalSensors, &SensorStore::sensorGotState);
connect(&mqttSensorSource, &MqttSensorSource::stateChanged, &globalSensors, &SensorStore::sensorGotState);
sunSensorSource.run();
@ -93,6 +94,9 @@ PrimaryMainObject::PrimaryMainObject(QIODevice* microDevice, const QString& sett
loadFromDisk(settingsPath);
QJsonObject mqttJson = settings["Mqtt"].toObject();
mqttSensorSource.start(mqttJson);
tcpServer->launch(QHostAddress(host), port);
connect(&globalItems, &ItemStore::itemUpdated, tcpServer, &TcpServer::itemUpdated);
}
@ -105,6 +109,9 @@ PrimaryMainObject::~PrimaryMainObject()
void PrimaryMainObject::store(QJsonObject &json)
{
globalItems.store(json);
QJsonObject mqttJson = json["Mqtt"].toObject();
mqttSensorSource.store(mqttJson);
json["Mqtt"] = mqttJson;
}
void PrimaryMainObject::load(const QJsonObject& json)

View file

@ -14,6 +14,7 @@
#include "microcontroller.h"
#include "ui/mainwindow.h"
#include "sensors/sunsensor.h"
#include "sensors/mqttsensorsource.h"
#include "items/fixeditemsource.h"
#include "items/itemloadersource.h"
#include "tcpserver.h"
@ -45,6 +46,7 @@ public:
//sensors
SunSensorSource sunSensorSource;
MqttSensorSource mqttSensorSource;
//item sources
FixedItemSource fixedItems;

View file

@ -0,0 +1,187 @@
#include "mqttsensorsource.h"
#include<QJsonArray>
MqttSensorSource::MqttSensorSource(QObject *parent)
: QObject{parent}
{
}
void MqttSensorSource::start(const QJsonObject& settings)
{
baseTopicName = settings["BaseTopic"].toString("zigbee2mqtt");
connect(&client, &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();
for(QJsonValueRef sensorRef : sensorsArray)
{
QJsonObject sensorObject = sensorRef.toObject();
if(!sensorObject.contains("Topic"))
continue;
SensorSubscription sensor;
sensor.topic = sensorObject["Topic"].toString();
if(!sensorObject.contains("Name"))
sensor.name = sensor.topic;
else
sensor.name = sensorObject["Name"].toString();
sensor.id = qHash(baseTopicName + "/" + sensor.topic);
sensors.push_back(sensor);
}
}
void MqttSensorSource::onClientError(QMqttClient::ClientError error)
{
qWarning()<<"MQTT Client error:"<<error;
}
MqttSensorSource::SensorSubscription& MqttSensorSource::findSubscription(const QString& topic)
{
for(SensorSubscription& sensor : sensors)
{
if(baseTopicName + "/" + sensor.topic == topic)
return sensor;
}
assert(false);
}
void MqttSensorSource::onClientStateChanged(QMqttClient::ClientState state)
{
if(state == QMqttClient::ClientState::Connected)
{
qInfo()<<"Connected to MQTT broker at "<<client.hostname()<<client.port();
for(SensorSubscription& sensor : sensors)
{
qDebug()<<"MQTT subscribeing to"<<baseTopicName + "/" + sensor.topic;
sensor.subscription = client.subscribe(baseTopicName + "/" + sensor.topic);
connect(sensor.subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived);
}
}
else if (state == QMqttClient::ClientState::Disconnected)
{
qWarning()<<"Lost connection to MQTT broker";
for(SensorSubscription& sensor : sensors)
{
if(sensor.subscription)
{
client.unsubscribe(sensor.topic);
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)
{
Sensor sensor;
SensorSubscription& sensorSub = findSubscription(message.topic().name());
QString baseName = sensorSub.name;
sensor.id = sensorSub.id;
sensor.updateSeen();
QJsonDocument doc = QJsonDocument::fromJson(message.payload());
if(doc.isObject())
{
QJsonObject obj = doc.object();
if(obj.contains("temperature"))
{
sensor.name = baseName + " Temperature";
sensor.type = Sensor::TYPE_TEMPERATURE;
sensor.field = obj["temperature"].toDouble(0);
stateChanged(sensor);
}
if(obj.contains("humidity"))
{
sensor.name = baseName + " Humidity";
sensor.type = Sensor::TYPE_HUMIDITY;
sensor.field = obj["humidity"].toDouble(0);
stateChanged(sensor);
}
if(obj.contains("illuminance"))
{
sensor.name = baseName + " Illuminance";
sensor.type = Sensor::TYPE_BRIGHTNESS;
sensor.field = obj["illuminance"].toDouble(0);
stateChanged(sensor);
}
if(obj.contains("presence"))
{
sensor.name = baseName + " Presence";
sensor.type = Sensor::TYPE_OCUPANCY;
sensor.field = obj["presence"].toBool() ? 1 : 0;
stateChanged(sensor);
}
if(obj.contains("co2"))
{
sensor.name = baseName + " co2";
sensor.type = Sensor::TYPE_CO2;
sensor.field = obj["co2"].toDouble(0);
stateChanged(sensor);
}
if(obj.contains("formaldehyd"))
{
sensor.name = baseName + " Formaldehyd";
sensor.type = Sensor::TYPE_FORMALDEHYD;
sensor.field = obj["formaldehyd"].toDouble(0);
stateChanged(sensor);
}
if(obj.contains("pm25"))
{
sensor.name = baseName + " pm25";
sensor.type = Sensor::TYPE_PM25;
sensor.field = obj["pm25"].toDouble(0);
stateChanged(sensor);
}
if(obj.contains("voc"))
{
sensor.name = baseName + " VOC";
sensor.type = Sensor::TYPE_TOTAL_VOC;
sensor.field = obj["voc"].toDouble(0);
stateChanged(sensor);
}
}
}
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;
for(const SensorSubscription& sensor : sensors)
{
QJsonObject sensorObject;
sensorObject["Name"] = sensor.name;
sensorObject["Topic"] = sensor.topic;
sensorsArray.append(sensorObject);
}
json["Sensors"] = sensorsArray;
}

View file

@ -0,0 +1,44 @@
#ifndef MQTTSENSORSOURCE_H
#define MQTTSENSORSOURCE_H
#include <QObject>
#include <QJsonObject>
#include <QtMqtt/QMqttClient>
#include <vector>
#include "sensor.h"
class MqttSensorSource : public QObject
{
Q_OBJECT
struct SensorSubscription
{
uint64_t id;
QString topic;
QString name;
QMqttSubscription* subscription = nullptr;
};
QString baseTopicName;
std::vector<SensorSubscription> sensors;
QMqttClient client;
private:
SensorSubscription& findSubscription(const QString& topic);
private slots:
void onClientStateChanged(QMqttClient::ClientState state);
void onMessageReceived(const QMqttMessage& message);
void onClientError(QMqttClient::ClientError error);
public:
explicit MqttSensorSource(QObject *parent = nullptr);
void start(const QJsonObject& settings);
void store(QJsonObject& json);
signals:
void stateChanged(Sensor sensor);
};
#endif // MQTTSENSORSOURCE_H

View file

@ -17,6 +17,10 @@ public:
static constexpr uint8_t TYPE_BRIGHTNESS = 4;
static constexpr uint8_t TYPE_BUTTON = 5;
static constexpr uint8_t TYPE_ADC = 6;
static constexpr uint8_t TYPE_CO2 = 7;
static constexpr uint8_t TYPE_FORMALDEHYD= 8;
static constexpr uint8_t TYPE_PM25 = 9;
static constexpr uint8_t TYPE_TOTAL_VOC = 10;
static constexpr uint8_t TYPE_LOWBATTERY = 128;
static constexpr uint8_t TYPE_SHUTDOWN_IMMINENT = 251;
static constexpr uint8_t TYPE_OCUPANCY = 252;
@ -25,13 +29,13 @@ public:
static constexpr uint8_t TYPE_DUMMY = 255;
uint8_t type;
uint8_t id;
uint64_t id;
float field;
QString name;
QDateTime lastSeen;
bool hidden;
Sensor(uint8_t typeIn, uint8_t idIn, float fieldIn = 0, QString nameIn = "", bool hiddenIn = false): type(typeIn),
Sensor(uint64_t typeIn, uint8_t idIn, float fieldIn = 0, QString nameIn = "", bool hiddenIn = false): type(typeIn),
id(idIn), field(fieldIn), name(nameIn), hidden(hiddenIn)
{
lastSeen = QDateTime::currentDateTime();

View file

@ -42,7 +42,7 @@
<property name="autoFillBackground">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5" stretch="1,0">
<layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">