UvosSmartHomeInterface/src/items/mqttitem.cpp

416 lines
9.9 KiB
C++

#include "mqttitem.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <QtMqtt/QMqttClient>
#include "mqttclient.h"
#include "programmode.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 || programMode == PROGRAM_MODE_UI_ONLY);
if(workClient)
connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged);
}
MqttItem::~MqttItem()
{
qDebug()<<__func__;
std::shared_ptr<MqttClient> workClient = client.lock();
if(!workClient)
return;
if(subscription)
{
disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
workClient->unsubscribe(subscription);
}
if(devicesSubscription)
{
disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
workClient->unsubscribe(devicesSubscription);
}
}
void MqttItem::onClientStateChanged(QMqttClient::ClientState state)
{
if(state == QMqttClient::Connected)
{
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()
{
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::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;
Q_EMIT exposeLoaded();
// 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)
{
QJsonDocument doc = QJsonDocument::fromJson(message.payload());
if(doc.isObject())
{
QJsonObject obj = doc.object();
if(obj.contains(getValueKey()))
{
QJsonValue value = obj[getValueKey()];
ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND);
req.changes.value = true;
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);
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;
}
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();
}
void MqttItem::triggerExposeLookup()
{
if(exposeLoaded_)
return;
std::shared_ptr<MqttClient> workClient = client.lock();
if(!workClient)
return;
// Reset expose loaded flag to allow re-detection
exposeLoaded_ = false;
// Subscribe to bridge/devices
if(devicesSubscription)
{
disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
workClient->unsubscribe(devicesSubscription);
devicesSubscription = nullptr;
}
devicesSubscription = workClient->subscribe(workClient->getBaseTopic() + "/bridge/devices");
connect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
}
QString MqttItem::getTopic() const
{
return topic_;
}
QString MqttItem::getValueKey() const
{
return valueKey_;
}
QString MqttItem::getValueOn() const
{
return valueOn_;
}
QString MqttItem::getValueOff() const
{
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)
{
Item::store(json);
json["Type"] = "Mqtt";
json["Topic"] = topic_;
json["ValueKey"] = valueKey_;
json["ValueOn"] = valueOn_;
json["ValueOff"] = valueOff_;
json["ValueMin"] = valueMin_;
json["ValueMax"] = valueMax_;
json["ValueStep"] = valueStep_;
}
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");
valueMin_ = json["ValueMin"].toInt(0);
valueMax_ = json["ValueMax"].toInt(255);
valueStep_ = json["ValueStep"].toInt(1);
exposeLoaded_ = json["ExposeLoaded"].toBool(false);
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;
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_;
}
QJsonDocument doc(payload);
QByteArray data = doc.toJson(QJsonDocument::Compact);
qDebug() << "MqttItem publishing to" << fullTopic << ":" << data;
workClient->getClient()->publish(fullTopic, data);
}