Fix mqtt item expose detection only working on local instances
This commit is contained in:
parent
45676b3384
commit
58ba22b267
24 changed files with 340 additions and 415 deletions
|
|
@ -6,9 +6,12 @@ project(smartvos VERSION 1.0 LANGUAGES CXX)
|
|||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Enable all warnings
|
||||
add_compile_options(-Wall)
|
||||
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "/usr" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Find Qt packages
|
||||
find_package(Qt6 COMPONENTS Core Gui Widgets Network Multimedia SerialPort Mqtt WebSockets REQUIRED)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
|
||||
#include <QJsonArray>
|
||||
|
||||
ItemData::ItemData(uint32_t itemIdIn, QString name, uint8_t value, bool loaded, bool hidden, item_value_type_t type, QString groupName):
|
||||
name_(name), value_(value), itemId_(itemIdIn), loaded_(loaded), hidden_(hidden), type_(type), groupName_(groupName)
|
||||
ItemData::ItemData(uint32_t itemIdIn, QString name, uint8_t value, bool hidden, item_value_type_t type, QString groupName):
|
||||
name_(name), value_(value), itemId_(itemIdIn), hidden_(hidden), type_(type), groupName_(groupName)
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -119,16 +119,6 @@ ItemFieldChanges ItemData::loadWithChanges(const QJsonObject& json, const bool p
|
|||
return changes;
|
||||
}
|
||||
|
||||
bool ItemData::getLoaded() const
|
||||
{
|
||||
return loaded_;
|
||||
}
|
||||
|
||||
void ItemData::setLoaded(bool loaded)
|
||||
{
|
||||
loaded_ = loaded;
|
||||
}
|
||||
|
||||
bool ItemData::hasChanged(const ItemData& other) const
|
||||
{
|
||||
ItemFieldChanges changes(true);
|
||||
|
|
@ -219,7 +209,7 @@ void ItemData::setOverride(bool overrideVal)
|
|||
//item
|
||||
|
||||
Item::Item(uint32_t itemIdIn, QString name, uint8_t value, QObject *parent): QObject(parent), ItemData (itemIdIn, name,
|
||||
value, false, false, ITEM_VALUE_BOOL, "All")
|
||||
value, false, ITEM_VALUE_BOOL, "All")
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -421,10 +411,7 @@ std::shared_ptr<Item> Item::loadItem(const QJsonObject& json)
|
|||
else
|
||||
qWarning()<<"Unable to load unkown item type: "<<json["Type"].toString();
|
||||
if(newItem)
|
||||
{
|
||||
newItem->load(json);
|
||||
newItem->setLoaded(true);
|
||||
}
|
||||
return newItem;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,26 @@ typedef enum {
|
|||
ITEM_UPDATE_INVALID
|
||||
} item_update_type_t;
|
||||
|
||||
|
||||
inline static const char* itemUpdateTypeToString(item_update_type_t type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case ITEM_UPDATE_USER:
|
||||
return "User";
|
||||
case ITEM_UPDATE_ACTOR:
|
||||
return "Actor";
|
||||
case ITEM_UPDATE_REMOTE:
|
||||
return "Remote";
|
||||
case ITEM_UPDATE_LOADED:
|
||||
return "Loaded";
|
||||
case ITEM_UPDATE_BACKEND:
|
||||
return "Backend";
|
||||
default:
|
||||
return "Invalid";
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemFieldChanges;
|
||||
struct ItemUpdateRequest;
|
||||
|
||||
|
|
@ -33,7 +53,6 @@ protected:
|
|||
QString name_;
|
||||
uint8_t value_;
|
||||
uint32_t itemId_;
|
||||
bool loaded_;
|
||||
bool hidden_;
|
||||
item_value_type_t type_;
|
||||
QString groupName_;
|
||||
|
|
@ -44,7 +63,6 @@ public:
|
|||
ItemData(uint32_t itemIdIn = QRandomGenerator::global()->generate(),
|
||||
QString name = "Item",
|
||||
uint8_t value = 0,
|
||||
bool loaded = false,
|
||||
bool hidden = false,
|
||||
item_value_type_t type = ITEM_VALUE_BOOL,
|
||||
QString groupName = "");
|
||||
|
|
@ -65,8 +83,6 @@ public:
|
|||
void setName(QString name);
|
||||
uint8_t getValue() const;
|
||||
void setValueData(uint8_t value);
|
||||
bool getLoaded() const;
|
||||
void setLoaded(bool loaded);
|
||||
bool isHidden() const;
|
||||
void setHidden(bool hidden);
|
||||
item_value_type_t getValueType();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ void ItemLoaderSource::refresh()
|
|||
std::shared_ptr<Item> newItem = Item::loadItem(itemObject);
|
||||
if(newItem)
|
||||
{
|
||||
qDebug()<<"Loaded item"<<newItem->getName();
|
||||
ItemAddRequest request;
|
||||
request.type = ITEM_UPDATE_LOADED;
|
||||
request.payload = newItem;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ void ItemStore::addItem(const ItemAddRequest& item)
|
|||
{
|
||||
items_.push_back(item.payload);
|
||||
connect(item.payload.get(), &Item::updated, this, &ItemStore::itemUpdateSlot);
|
||||
qDebug()<<"Item"<<item.payload->getName()<<"added"<<(item.payload->getLoaded() ? "from loaded" : "");
|
||||
qDebug()<<"Item"<<item.payload->getName()<<"added with id"<<item.payload->id()<<"from"<<itemUpdateTypeToString(item.type);
|
||||
itemAdded(std::weak_ptr<Item>(items_.back()));
|
||||
}
|
||||
else if(!item.changes.isNone())
|
||||
|
|
|
|||
|
|
@ -35,27 +35,12 @@ MqttItem::~MqttItem()
|
|||
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()
|
||||
|
|
@ -77,53 +62,89 @@ void MqttItem::refreshSubscription()
|
|||
connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
|
||||
}
|
||||
|
||||
void MqttItem::onDevicesMessageReceived(const QMqttMessage& message)
|
||||
bool MqttItem::setFromDevices(const QJsonArray& devices)
|
||||
{
|
||||
QJsonObject device = getSelfFromDevices(devices);
|
||||
if(device.empty())
|
||||
return false;
|
||||
return loadExposeFromDevice(device);
|
||||
}
|
||||
|
||||
std::vector<QString> MqttItem::getAvailableValueKeys(const QJsonArray& devices)
|
||||
{
|
||||
QJsonObject device = getSelfFromDevices(devices);
|
||||
if(device.empty())
|
||||
return std::vector<QString>();
|
||||
|
||||
QJsonObject definition = device["definition"].toObject();
|
||||
if(definition.isEmpty())
|
||||
{
|
||||
qWarning() << "MqttItem" << topic_ << "device has no definition (unsupported)";
|
||||
return std::vector<QString>();
|
||||
}
|
||||
|
||||
QJsonArray exposes = definition["exposes"].toArray();
|
||||
if(exposes.isEmpty())
|
||||
{
|
||||
qWarning() << "MqttItem" << topic_ << "device has no exposes";
|
||||
return std::vector<QString>();
|
||||
}
|
||||
|
||||
std::vector<QString> values;
|
||||
for(const QJsonValue& exposeValue : exposes)
|
||||
{
|
||||
if(!exposeValue.isObject())
|
||||
continue;
|
||||
|
||||
QJsonObject expose = exposeValue.toObject();
|
||||
if(expose.contains("property"))
|
||||
values.push_back(expose["property"].toString());
|
||||
|
||||
// Check if it's a composite type with features
|
||||
if(expose.contains("features"))
|
||||
{
|
||||
QJsonArray features = expose["features"].toArray();
|
||||
for(const QJsonValue& featureValue : features)
|
||||
{
|
||||
if(!featureValue.isObject())
|
||||
continue;
|
||||
|
||||
QJsonObject feature = featureValue.toObject();
|
||||
if(feature["property"].toString() == valueKey_)
|
||||
values.push_back(feature["property"].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
QJsonObject MqttItem::getSelfFromDevices(const QJsonArray& devices)
|
||||
{
|
||||
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;
|
||||
}
|
||||
return device;
|
||||
}
|
||||
qWarning()<<"Could not find own topic in devices array";
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
void MqttItem::loadExposeFromDevice(const QJsonObject& device)
|
||||
bool 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get exposes from definition
|
||||
|
|
@ -131,7 +152,7 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device)
|
|||
if(exposes.isEmpty())
|
||||
{
|
||||
qWarning() << "MqttItem" << topic_ << "device has no exposes";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for(const QJsonValue& exposeValue : exposes)
|
||||
|
|
@ -147,11 +168,11 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device)
|
|||
{
|
||||
setFromExpose(expose);
|
||||
qDebug() << "MqttItem" << topic_ << "detected type" << expose["type"].toString() << "for property" << valueKey_;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a composite type with features
|
||||
if(expose["type"].toString() == "composite" || expose["type"].toString() == "light")
|
||||
if(expose.contains("features"))
|
||||
{
|
||||
QJsonArray features = expose["features"].toArray();
|
||||
for(const QJsonValue& featureValue : features)
|
||||
|
|
@ -164,13 +185,14 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device)
|
|||
{
|
||||
setFromExpose(feature);
|
||||
qDebug() << "MqttItem" << topic_ << "detected type" << feature["type"].toString() << "for property" << valueKey_;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "MqttItem" << topic_ << "could not find expose for property" << valueKey_;
|
||||
qWarning()<<"MqttItem"<<topic_<<"could not find expose for property"<< valueKey_;
|
||||
return false;
|
||||
}
|
||||
|
||||
void MqttItem::onMessageReceived(const QMqttMessage& message)
|
||||
|
|
@ -294,30 +316,6 @@ void MqttItem::setFromExpose(const QJsonObject& expose)
|
|||
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_;
|
||||
|
|
@ -353,11 +351,6 @@ int MqttItem::getValueStep() const
|
|||
return valueStep_;
|
||||
}
|
||||
|
||||
bool MqttItem::getExposeLoaded() const
|
||||
{
|
||||
return exposeLoaded_;
|
||||
}
|
||||
|
||||
void MqttItem::store(QJsonObject& json)
|
||||
{
|
||||
Item::store(json);
|
||||
|
|
@ -381,7 +374,6 @@ void MqttItem::load(const QJsonObject& json, const bool preserve)
|
|||
valueMin_ = json["ValueMin"].toInt(0);
|
||||
valueMax_ = json["ValueMax"].toInt(255);
|
||||
valueStep_ = json["ValueStep"].toInt(1);
|
||||
exposeLoaded_ = json["ExposeLoaded"].toBool(false);
|
||||
hashId();
|
||||
refreshSubscription();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ class QString;
|
|||
class MqttItem : public Item
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_SIGNALS:
|
||||
void exposeLoaded();
|
||||
|
||||
public:
|
||||
inline static std::weak_ptr<MqttClient> client;
|
||||
|
|
@ -23,17 +21,17 @@ private:
|
|||
int valueMin_ = 0;
|
||||
int valueMax_ = 255;
|
||||
int valueStep_ = 1;
|
||||
bool exposeLoaded_ = false;
|
||||
|
||||
MqttClient::Subscription* subscription = nullptr;
|
||||
MqttClient::Subscription* devicesSubscription = nullptr;
|
||||
|
||||
void hashId();
|
||||
void refreshSubscription();
|
||||
void onMessageReceived(const QMqttMessage& message);
|
||||
void onClientStateChanged(QMqttClient::ClientState state);
|
||||
void onDevicesMessageReceived(const QMqttMessage& message);
|
||||
void loadExposeFromDevice(const QJsonObject& device);
|
||||
|
||||
bool loadExposeFromDevice(const QJsonObject& device);
|
||||
void setFromExpose(const QJsonObject& expose);
|
||||
QJsonObject getSelfFromDevices(const QJsonArray& devices);
|
||||
|
||||
public:
|
||||
explicit MqttItem(QString name = "MqttItem",
|
||||
|
|
@ -51,11 +49,8 @@ public:
|
|||
void setValueStep(int step);
|
||||
void setValueType(item_value_type_t type);
|
||||
|
||||
// Configure from Zigbee2MQTT expose info
|
||||
void setFromExpose(const QJsonObject& expose);
|
||||
|
||||
// Trigger expose lookup from bridge/devices
|
||||
void triggerExposeLookup();
|
||||
bool setFromDevices(const QJsonArray& devices);
|
||||
std::vector<QString> getAvailableValueKeys(const QJsonArray& devices);
|
||||
|
||||
QString getTopic() const;
|
||||
QString getValueKey() const;
|
||||
|
|
@ -64,7 +59,6 @@ public:
|
|||
int getValueMin() const;
|
||||
int getValueMax() const;
|
||||
int getValueStep() const;
|
||||
bool getExposeLoaded() const;
|
||||
|
||||
virtual void store(QJsonObject& json) override;
|
||||
virtual void load(const QJsonObject& json, const bool preserve = false) override;
|
||||
|
|
@ -73,4 +67,4 @@ protected:
|
|||
virtual void enactValue(uint8_t value) override;
|
||||
};
|
||||
|
||||
#endif // MQTTITEM_H
|
||||
#endif // MQTTITEM_H
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "mqttclient.h"
|
||||
#include "items/mqttitem.h"
|
||||
#include "items/itemstore.h"
|
||||
#include "ui/itemsettingswidgets/mqttitemsettingswidget.h"
|
||||
|
||||
MainObject::MainObject(QObject *parent) :
|
||||
QObject(parent)
|
||||
|
|
@ -176,6 +177,8 @@ SecondaryMainObject::SecondaryMainObject(QString host, int port, QObject *parent
|
|||
QMetaObject::invokeMethod(this, [](){exit(1);}, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
MqttItemSettingsWidget::service = tcpClient;
|
||||
|
||||
connect(&globalItems, &ItemStore::itemUpdated, tcpClient, &TcpClient::itemUpdated);
|
||||
|
||||
globalItems.refresh();
|
||||
|
|
|
|||
|
|
@ -114,7 +114,6 @@ Microcontroller::Microcontroller()
|
|||
writeTimer.setInterval(50);
|
||||
writeTimer.setSingleShot(false);
|
||||
connect(&writeTimer, &QTimer::timeout, this, &Microcontroller::onWriteTimerTimeout);
|
||||
qDebug()<<__func__<<writeTimer.isActive();
|
||||
}
|
||||
|
||||
Microcontroller::~Microcontroller()
|
||||
|
|
@ -139,10 +138,11 @@ std::shared_ptr<Relay> Microcontroller::processRelayLine(const QString& buffer)
|
|||
name.remove(name.size()-1, 1);
|
||||
else
|
||||
name = "Relay " + QString::number(bufferList[1].toInt(nullptr, 2));
|
||||
return std::shared_ptr<Relay>(new Relay(bufferList[2].toInt(),
|
||||
std::shared_ptr<Relay> relay(new Relay(bufferList[2].toInt(),
|
||||
name,
|
||||
bufferList[6].toInt(nullptr, 2),
|
||||
bufferList[4].toInt(nullptr, 2),
|
||||
bufferList[8].toInt()));
|
||||
return relay;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,20 @@ void MqttClient::start(const QJsonObject& settings)
|
|||
client->connectToHost();
|
||||
}
|
||||
|
||||
void MqttClient::onDevicesMessageReceived(const QMqttMessage& message)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(message.payload());
|
||||
if(!doc.isArray())
|
||||
return;
|
||||
qDebug()<<"MqttClient got devices array";
|
||||
devices = doc.array();
|
||||
}
|
||||
|
||||
QJsonArray MqttClient::getDevicesArray()
|
||||
{
|
||||
return devices;
|
||||
}
|
||||
|
||||
void MqttClient::onClientError(QMqttClient::ClientError error)
|
||||
{
|
||||
qWarning()<<"MQTT Client error:"<<error;
|
||||
|
|
@ -34,11 +48,19 @@ void MqttClient::onClientError(QMqttClient::ClientError error)
|
|||
void MqttClient::onClientStateChanged(QMqttClient::ClientState state)
|
||||
{
|
||||
if(state == QMqttClient::ClientState::Connected)
|
||||
{
|
||||
devicesSubscription = subscribe(getBaseTopic() + "/bridge/devices");
|
||||
connect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttClient::onDevicesMessageReceived);
|
||||
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)
|
||||
|
|
@ -107,9 +129,11 @@ QString MqttClient::getBaseTopic()
|
|||
|
||||
MqttClient::~MqttClient()
|
||||
{
|
||||
if(devicesSubscription)
|
||||
unsubscribe(devicesSubscription);
|
||||
for(const std::pair<QString, Subscription*> sub : subscriptions)
|
||||
{
|
||||
qWarning()<<sub.first<<"not unregistered at exit!";
|
||||
client->unsubscribe(sub.second->subscription->topic());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QMqttClient>
|
||||
#include <QObject>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <map>
|
||||
|
||||
class MqttClient: public QObject
|
||||
|
|
@ -20,10 +21,13 @@ private:
|
|||
QString baseTopicName;
|
||||
std::shared_ptr<QMqttClient> client;
|
||||
std::map<QString, Subscription*> subscriptions;
|
||||
Subscription* devicesSubscription = nullptr;
|
||||
QJsonArray devices;
|
||||
|
||||
private slots:
|
||||
void onClientStateChanged(QMqttClient::ClientState state);
|
||||
void onClientError(QMqttClient::ClientError error);
|
||||
void onDevicesMessageReceived(const QMqttMessage& message);
|
||||
|
||||
public:
|
||||
explicit MqttClient();
|
||||
|
|
@ -35,6 +39,7 @@ public:
|
|||
void unsubscribe(Subscription* subscription);
|
||||
void unsubscribe(QString topic);
|
||||
QString getBaseTopic();
|
||||
QJsonArray getDevicesArray();
|
||||
};
|
||||
|
||||
#endif // MQTTCLIENT_H
|
||||
|
|
|
|||
|
|
@ -5,6 +5,32 @@
|
|||
|
||||
SensorStore globalSensors;
|
||||
|
||||
QString sensorUpdateTypeToString(sensor_update_type_t type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case SENSOR_UPDATE_USER:
|
||||
return "User";
|
||||
case SENSOR_UPDATE_REMOTE:
|
||||
return "Remote";
|
||||
case SENSOR_UPDATE_BACKEND:
|
||||
return "Backend";
|
||||
default:
|
||||
return "Invalid";
|
||||
}
|
||||
}
|
||||
|
||||
sensor_update_type_t sensorUpdateTypeFromString(const QString& string)
|
||||
{
|
||||
if(string == sensorUpdateTypeToString(SENSOR_UPDATE_USER))
|
||||
return SENSOR_UPDATE_USER;
|
||||
if(string == sensorUpdateTypeToString(SENSOR_UPDATE_REMOTE))
|
||||
return SENSOR_UPDATE_REMOTE;
|
||||
if(string == sensorUpdateTypeToString(SENSOR_UPDATE_BACKEND))
|
||||
return SENSOR_UPDATE_BACKEND;
|
||||
return SENSOR_UPDATE_INVALID;
|
||||
}
|
||||
|
||||
SensorStore::SensorStore(QObject *parent): QObject(parent)
|
||||
{
|
||||
}
|
||||
|
|
@ -12,7 +38,13 @@ SensorStore::SensorStore(QObject *parent): QObject(parent)
|
|||
void SensorStore::store(QJsonObject& json)
|
||||
{
|
||||
QJsonArray sensorsArray;
|
||||
for(const Sensor& sensor : sensors_)
|
||||
std::vector<Sensor> sensors = sensors_;
|
||||
for(const Sensor& sensor : knownSensors_)
|
||||
{
|
||||
if(std::find(sensors.begin(), sensors.end(), sensor) == sensors.end())
|
||||
sensors.push_back(sensor);
|
||||
}
|
||||
for(const Sensor& sensor : sensors)
|
||||
{
|
||||
QJsonObject sensorObject;
|
||||
sensor.store(sensorObject);
|
||||
|
|
|
|||
|
|
@ -176,6 +176,9 @@ typedef enum {
|
|||
SENSOR_UPDATE_INVALID
|
||||
} sensor_update_type_t;
|
||||
|
||||
QString sensorUpdateTypeToString(sensor_update_type_t type);
|
||||
sensor_update_type_t sensorUpdateTypeFromString(const QString& string);
|
||||
|
||||
class SensorStore: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <QJsonArray>
|
||||
|
||||
#include "items/item.h"
|
||||
#include "items/mqttitem.h"
|
||||
#include "service.h"
|
||||
#include "server.h"
|
||||
|
||||
|
|
@ -33,7 +34,6 @@ void Server::processIncomeingJson(const QByteArray& jsonbytes)
|
|||
if(item)
|
||||
{
|
||||
qDebug()<<"Server got item"<<item->getName();
|
||||
item->setLoaded(FullList);
|
||||
fieldChanges.push_back(item->loadWithChanges(jsonobject));
|
||||
items.push_back(item);
|
||||
}
|
||||
|
|
@ -57,12 +57,24 @@ void Server::processIncomeingJson(const QByteArray& jsonbytes)
|
|||
updateItems(updates);
|
||||
}
|
||||
}
|
||||
else if(type == "GetMqttDevices")
|
||||
{
|
||||
sendMqttDevices();
|
||||
}
|
||||
else
|
||||
{
|
||||
Service::processIncomeingJson(jsonbytes);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::sendMqttDevices()
|
||||
{
|
||||
std::weak_ptr<MqttClient> client = MqttItem::client;
|
||||
std::shared_ptr<MqttClient> workClient = client.lock();
|
||||
if(workClient)
|
||||
sendJson(createMessage("MqttDevices", workClient->getDevicesArray()));
|
||||
}
|
||||
|
||||
void Server::handleSocketError()
|
||||
{
|
||||
QObject* obj = sender();
|
||||
|
|
@ -112,4 +124,4 @@ void Server::itemUpdated(ItemUpdateRequest update)
|
|||
QJsonObject json = createMessage("ItemUpdate", items);
|
||||
json["FullList"] = false;
|
||||
sendJson(json);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ protected:
|
|||
void handleSocketDisconnect();
|
||||
void removeClient(QTcpSocket* socket);
|
||||
void removeClient(QWebSocket* socket);
|
||||
void sendMqttDevices();
|
||||
|
||||
signals:
|
||||
void sigRequestSave();
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ void Service::refresh()
|
|||
sendJson(createMessage("GetItems", QJsonArray()));
|
||||
}
|
||||
|
||||
void Service::requestMqttDevices()
|
||||
{
|
||||
qDebug()<<__func__;
|
||||
sendJson(createMessage("GetMqttDevices", QJsonArray()));
|
||||
}
|
||||
|
||||
void Service::sendSensors()
|
||||
{
|
||||
QJsonArray sensors;
|
||||
|
|
@ -96,7 +102,17 @@ void Service::processIncomeingJson(const QByteArray& jsonbytes)
|
|||
{
|
||||
QJsonObject jsonobject = sensorjson.toObject();
|
||||
Sensor sensor(jsonobject);
|
||||
gotSensor(sensor, SENSOR_UPDATE_REMOTE);
|
||||
sensor_update_type_t updateType = SENSOR_UPDATE_REMOTE;
|
||||
if(jsonobject.contains("UpdateType"))
|
||||
{
|
||||
updateType = sensorUpdateTypeFromString(jsonobject["UpdateType"].toString());
|
||||
if(updateType == SENSOR_UPDATE_INVALID)
|
||||
{
|
||||
qWarning()<<"Got invalid sensor update type"<<jsonobject["UpdateType"].toString()<<"from remote, ignoreing";
|
||||
updateType = SENSOR_UPDATE_REMOTE;
|
||||
}
|
||||
}
|
||||
gotSensor(sensor, updateType);
|
||||
}
|
||||
}
|
||||
else if(type == "AddSensor")
|
||||
|
|
@ -107,4 +123,10 @@ void Service::processIncomeingJson(const QByteArray& jsonbytes)
|
|||
QJsonObject payload = json["Payload"].toObject();
|
||||
emit sensorAdded(sensor, backend, payload);
|
||||
}
|
||||
else if(type == "MqttDevices")
|
||||
{
|
||||
qDebug()<<"Got mqtt devices";
|
||||
QJsonArray payload = json["Data"].toArray();
|
||||
gotMqttDevices(payload);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ protected:
|
|||
signals:
|
||||
void gotSensor(Sensor sensor, sensor_update_type_t type = SENSOR_UPDATE_BACKEND);
|
||||
void sensorAdded(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload);
|
||||
void gotMqttDevices(QJsonArray devices);
|
||||
|
||||
public slots:
|
||||
virtual void sensorEvent(Sensor sensor, sensor_update_type_t type);
|
||||
virtual void itemUpdated(ItemUpdateRequest update);
|
||||
virtual void refresh() override;
|
||||
virtual void addSensor(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload = {});
|
||||
void requestMqttDevices();
|
||||
|
||||
public:
|
||||
Service(QObject* parent = nullptr);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ void TcpClient::processIncomeingJson(const QByteArray& jsonbytes)
|
|||
std::shared_ptr<Item> item = Item::loadItem(jsonobject);
|
||||
if(item)
|
||||
{
|
||||
item->setLoaded(FullList);
|
||||
fieldChanges.push_back(item->loadWithChanges(jsonobject));
|
||||
items.push_back(item);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
#include "mqttitemsettingswidget.h"
|
||||
#include "ui_mqttitemsettingswidget.h"
|
||||
#include "programmode.h"
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QDebug>
|
||||
|
||||
MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent) :
|
||||
|
|
@ -39,43 +41,6 @@ MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWi
|
|||
updateValueNamesFromItem();
|
||||
suppressUpdates_ = false;
|
||||
updateVisibility();
|
||||
|
||||
// Connect expose loaded signal
|
||||
connect(workingItem.get(), &MqttItem::exposeLoaded, this, [this]() {
|
||||
if(auto item = item_.lock())
|
||||
{
|
||||
suppressUpdates_ = true;
|
||||
ui->label_status->setText("Detected!");
|
||||
|
||||
// Update value type
|
||||
switch(item->getValueType())
|
||||
{
|
||||
case ITEM_VALUE_UINT:
|
||||
ui->comboBox_valueType->setCurrentIndex(1);
|
||||
break;
|
||||
case ITEM_VALUE_ENUM:
|
||||
ui->comboBox_valueType->setCurrentIndex(2);
|
||||
break;
|
||||
default:
|
||||
ui->comboBox_valueType->setCurrentIndex(0);
|
||||
break;
|
||||
}
|
||||
|
||||
// Update limits
|
||||
ui->spinBox_min->setValue(item->getValueMin());
|
||||
ui->spinBox_max->setValue(item->getValueMax());
|
||||
ui->spinBox_step->setValue(item->getValueStep());
|
||||
|
||||
// Update value on/off
|
||||
ui->lineEdit_valueOn->setText(item->getValueOn());
|
||||
ui->lineEdit_valueOff->setText(item->getValueOff());
|
||||
|
||||
// Update value names
|
||||
updateValueNamesFromItem();
|
||||
suppressUpdates_ = false;
|
||||
updateVisibility();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Connect signals
|
||||
|
|
@ -91,6 +56,60 @@ MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWi
|
|||
connect(ui->pushButton_addValueName, &QPushButton::clicked, this, &MqttItemSettingsWidget::onAddValueName);
|
||||
connect(ui->pushButton_removeValueName, &QPushButton::clicked, this, &MqttItemSettingsWidget::onRemoveValueName);
|
||||
connect(ui->listWidget_valueNames, &QListWidget::itemChanged, this, &MqttItemSettingsWidget::onValueNamesChanged);
|
||||
|
||||
if(service)
|
||||
connect(service, &Service::gotMqttDevices, this, &MqttItemSettingsWidget::onGotMqttDevices);
|
||||
else if(programMode == PROGRAM_MODE_UI_ONLY)
|
||||
qWarning()<<__func__<<"with no service available";
|
||||
}
|
||||
|
||||
void MqttItemSettingsWidget::onGotMqttDevices(QJsonArray devices)
|
||||
{
|
||||
if(auto item = item_.lock())
|
||||
{
|
||||
bool ret = item->setFromDevices(devices);
|
||||
if(!ret)
|
||||
QMessageBox::warning(this, "Unable to get expose", "Unable to load expose from devices");
|
||||
else
|
||||
exposeLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
void MqttItemSettingsWidget::exposeLoaded()
|
||||
{
|
||||
if(auto item = item_.lock())
|
||||
{
|
||||
suppressUpdates_ = true;
|
||||
ui->label_status->setText("Detected!");
|
||||
|
||||
// Update value type
|
||||
switch(item->getValueType())
|
||||
{
|
||||
case ITEM_VALUE_UINT:
|
||||
ui->comboBox_valueType->setCurrentIndex(1);
|
||||
break;
|
||||
case ITEM_VALUE_ENUM:
|
||||
ui->comboBox_valueType->setCurrentIndex(2);
|
||||
break;
|
||||
default:
|
||||
ui->comboBox_valueType->setCurrentIndex(0);
|
||||
break;
|
||||
}
|
||||
|
||||
// Update limits
|
||||
ui->spinBox_min->setValue(item->getValueMin());
|
||||
ui->spinBox_max->setValue(item->getValueMax());
|
||||
ui->spinBox_step->setValue(item->getValueStep());
|
||||
|
||||
// Update value on/off
|
||||
ui->lineEdit_valueOn->setText(item->getValueOn());
|
||||
ui->lineEdit_valueOff->setText(item->getValueOff());
|
||||
|
||||
// Update value names
|
||||
updateValueNamesFromItem();
|
||||
suppressUpdates_ = false;
|
||||
updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
void MqttItemSettingsWidget::setTopic(const QString& topic)
|
||||
|
|
@ -186,7 +205,24 @@ void MqttItemSettingsWidget::onAutoDetectClicked()
|
|||
if(auto workingItem = item_.lock())
|
||||
{
|
||||
ui->label_status->setText("Detecting...");
|
||||
workingItem->triggerExposeLookup();
|
||||
std::shared_ptr<MqttClient> workClient = MqttItem::client.lock();
|
||||
if(workClient)
|
||||
{
|
||||
bool ret = workingItem->setFromDevices(workClient->getDevicesArray());
|
||||
if(!ret)
|
||||
{
|
||||
QMessageBox::warning(this, "Unable to get expose", "Unable to load expose from devices");
|
||||
qDebug()<<"Has exposes:"<<workingItem->getAvailableValueKeys(workClient->getDevicesArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
exposeLoaded();
|
||||
}
|
||||
}
|
||||
else if(service)
|
||||
{
|
||||
service->requestMqttDevices();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -269,4 +305,4 @@ void MqttItemSettingsWidget::updateValueNamesFromItem()
|
|||
MqttItemSettingsWidget::~MqttItemSettingsWidget()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QWidget>
|
||||
#include <memory>
|
||||
#include "../../items/mqttitem.h"
|
||||
#include "../../service/service.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
|
|
@ -13,9 +14,17 @@ class MqttItemSettingsWidget;
|
|||
class MqttItemSettingsWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
inline static Service* service = nullptr;
|
||||
|
||||
private:
|
||||
|
||||
std::weak_ptr<MqttItem> item_;
|
||||
bool suppressUpdates_ = false;
|
||||
|
||||
void exposeLoaded();
|
||||
|
||||
private slots:
|
||||
void setTopic(const QString& topic);
|
||||
void setValueKey(const QString& valueKey);
|
||||
|
|
@ -29,6 +38,7 @@ private slots:
|
|||
void onAddValueName();
|
||||
void onRemoveValueName();
|
||||
void onValueNamesChanged();
|
||||
void onGotMqttDevices(QJsonArray devices);
|
||||
|
||||
public:
|
||||
explicit MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent = nullptr);
|
||||
|
|
@ -41,4 +51,4 @@ private:
|
|||
void syncValueNamesToItem();
|
||||
};
|
||||
|
||||
#endif // MQTTITEMSETTINGSWIDGET_H
|
||||
#endif // MQTTITEMSETTINGSWIDGET_H
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ ItemWidget::ItemWidget(std::weak_ptr<Item> item, bool noGroupEdit, QWidget *pare
|
|||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
connect(&valueTimer, &QTimer::timeout, this, &ItemWidget::onValueTimerTimeout);
|
||||
|
||||
if(auto workingItem = item_.lock())
|
||||
{
|
||||
if(workingItem->getValueType() == ITEM_VALUE_UINT)
|
||||
|
|
@ -76,6 +78,21 @@ void ItemWidget::deleteItem()
|
|||
}
|
||||
|
||||
void ItemWidget::moveToValue(int value)
|
||||
{
|
||||
valueQue.enqueue(value);
|
||||
if(!valueTimer.isActive())
|
||||
{
|
||||
valueTimer.setInterval(0);
|
||||
valueTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void ItemWidget::moveToState(bool state)
|
||||
{
|
||||
moveToValue(state);
|
||||
}
|
||||
|
||||
void ItemWidget::applyValue(int value)
|
||||
{
|
||||
if(auto workingItem = item_.lock())
|
||||
{
|
||||
|
|
@ -90,9 +107,19 @@ void ItemWidget::moveToValue(int value)
|
|||
}
|
||||
}
|
||||
|
||||
void ItemWidget::moveToState(bool state)
|
||||
void ItemWidget::onValueTimerTimeout()
|
||||
{
|
||||
moveToValue(state);
|
||||
valueTimer.setInterval(50);
|
||||
if(!valueQue.empty())
|
||||
{
|
||||
while(valueQue.count() > 2)
|
||||
valueQue.dequeue();
|
||||
applyValue(valueQue.dequeue());
|
||||
}
|
||||
else
|
||||
{
|
||||
valueTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ItemWidget::disable()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#define RELAYWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QQueue>
|
||||
#include <QTimer>
|
||||
#include <memory>
|
||||
#include "../items/item.h"
|
||||
|
||||
|
|
@ -17,6 +19,9 @@ private:
|
|||
std::weak_ptr<Item> item_;
|
||||
bool noGroupEdit_;
|
||||
|
||||
QQueue<int> valueQue;
|
||||
QTimer valueTimer;
|
||||
|
||||
void disable();
|
||||
|
||||
signals:
|
||||
|
|
@ -28,6 +33,7 @@ private slots:
|
|||
void moveToState(bool state);
|
||||
void moveToValue(int value);
|
||||
void deleteItem();
|
||||
void onValueTimerTimeout();
|
||||
|
||||
public:
|
||||
explicit ItemWidget(std::weak_ptr<Item> item, bool noGroupEdit = false, QWidget *parent = nullptr);
|
||||
|
|
@ -41,6 +47,7 @@ public slots:
|
|||
void onItemUpdated(ItemUpdateRequest update);
|
||||
|
||||
private:
|
||||
void applyValue(int value);
|
||||
Ui::ItemWidget *ui;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -73,15 +73,6 @@ private slots:
|
|||
QVERIFY(!item.isHidden());
|
||||
}
|
||||
|
||||
void testItemLoaded()
|
||||
{
|
||||
Item item(1, "test_item", 0);
|
||||
QVERIFY(!item.getLoaded());
|
||||
|
||||
item.setLoaded(true);
|
||||
QVERIFY(item.getLoaded());
|
||||
}
|
||||
|
||||
void testItemJsonSerialization()
|
||||
{
|
||||
Item item(42, "test_item", 1);
|
||||
|
|
|
|||
|
|
@ -138,66 +138,6 @@ private slots:
|
|||
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);
|
||||
|
|
@ -237,186 +177,6 @@ private slots:
|
|||
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);
|
||||
}
|
||||
|
||||
// Note: Full integration tests for onDevicesMessageReceived require QMqttMessage construction
|
||||
// which is not possible without making it a friend. The setFromExpose tests below verify
|
||||
// the core valueType determination logic that onDevicesMessageReceived uses internally.
|
||||
// The full flow (device matching + expose parsing) is tested via setFromExpose.
|
||||
|
||||
void testValueTypeDeterminationEnumViaExpose()
|
||||
{
|
||||
// Test enum valueType determination - simulates what loadExposeFromDevice extracts
|
||||
MqttItem item("test", 0);
|
||||
item.setTopic("0xa4c138ef510950e3");
|
||||
item.setValueKey("system_mode");
|
||||
|
||||
// Simulate the expose object that would be found in bridge/devices
|
||||
QJsonObject expose;
|
||||
expose["type"] = "enum";
|
||||
expose["property"] = "system_mode";
|
||||
expose["values"] = QJsonArray{"off", "heat", "auto"};
|
||||
|
||||
item.setFromExpose(expose);
|
||||
|
||||
QVERIFY2(item.getValueType() == ITEM_VALUE_ENUM, "ValueType should be ENUM");
|
||||
QVERIFY2(item.getValueKey() == "system_mode", "ValueKey should be set");
|
||||
|
||||
auto names = item.getValueNames();
|
||||
QVERIFY2(names.size() == 3, "Should have 3 enum values");
|
||||
QVERIFY2(names[0] == "off", "First value should be 'off'");
|
||||
QVERIFY2(names[1] == "heat", "Second value should be 'heat'");
|
||||
QVERIFY2(names[2] == "auto", "Third value should be 'auto'");
|
||||
}
|
||||
|
||||
void testValueTypeDeterminationNumericViaExpose()
|
||||
{
|
||||
// Test numeric valueType determination
|
||||
MqttItem item("test", 0);
|
||||
item.setTopic("0xa4c138d9a039b6df");
|
||||
item.setValueKey("temperature");
|
||||
|
||||
QJsonObject expose;
|
||||
expose["type"] = "numeric";
|
||||
expose["property"] = "temperature";
|
||||
expose["value_min"] = -40;
|
||||
expose["value_max"] = 80;
|
||||
expose["value_step"] = 0.1; // Note: toInt() on double returns default, so step becomes 1
|
||||
|
||||
item.setFromExpose(expose);
|
||||
|
||||
QVERIFY2(item.getValueType() == ITEM_VALUE_UINT, "ValueType should be UINT");
|
||||
QVERIFY2(item.getValueMin() == -40, "Min should be -40");
|
||||
QVERIFY2(item.getValueMax() == 80, "Max should be 80");
|
||||
QVERIFY2(item.getValueStep() == 1, "Step should be 1 (toInt on double returns default)");
|
||||
}
|
||||
|
||||
void testValueTypeDeterminationBinaryViaExpose()
|
||||
{
|
||||
// Test binary valueType determination
|
||||
MqttItem item("test", 0);
|
||||
item.setTopic("0xa4c138f3d3cf8700");
|
||||
item.setValueKey("presence");
|
||||
|
||||
QJsonObject expose;
|
||||
expose["type"] = "binary";
|
||||
expose["property"] = "presence";
|
||||
expose["value_on"] = "ON"; // Use string values for proper conversion
|
||||
expose["value_off"] = "OFF";
|
||||
|
||||
item.setFromExpose(expose);
|
||||
|
||||
QVERIFY2(item.getValueType() == ITEM_VALUE_BOOL, "ValueType should be BOOL");
|
||||
QVERIFY2(item.getValueOn() == "ON", "ValueOn should be 'ON'");
|
||||
QVERIFY2(item.getValueOff() == "OFF", "ValueOff should be 'OFF'");
|
||||
}
|
||||
|
||||
void testValueTypeDeterminationCompositeFeatureViaExpose()
|
||||
{
|
||||
// Test composite/climate feature valueType determination
|
||||
MqttItem item("test", 0);
|
||||
item.setTopic("0xa4c138ef510950e3");
|
||||
item.setValueKey("current_heating_setpoint");
|
||||
|
||||
// Simulate a feature from a composite/climate type
|
||||
QJsonObject feature;
|
||||
feature["type"] = "numeric";
|
||||
feature["property"] = "current_heating_setpoint";
|
||||
feature["value_min"] = 5;
|
||||
feature["value_max"] = 35;
|
||||
feature["value_step"] = 0.5;
|
||||
|
||||
item.setFromExpose(feature);
|
||||
|
||||
QVERIFY2(item.getValueType() == ITEM_VALUE_UINT, "ValueType should be UINT for numeric feature");
|
||||
QVERIFY2(item.getValueMin() == 5, "Min should be 5");
|
||||
QVERIFY2(item.getValueMax() == 35, "Max should be 35");
|
||||
}
|
||||
|
||||
void testRealDeviceExposeFromMqttBroker()
|
||||
{
|
||||
// Integration test: Verify valueType determination works with real device data
|
||||
// from the MQTT broker. This tests the actual zigbee2mqtt bridge/devices format.
|
||||
|
||||
// Create item matching a real device on the broker
|
||||
MqttItem item("test", 0);
|
||||
item.setTopic("0xa4c138ef510950e3");
|
||||
item.setValueKey("system_mode");
|
||||
|
||||
// The real device has system_mode as an enum with values ["auto", "heat", "off"]
|
||||
// This matches the actual expose from zigbee2mqtt/bridge/devices
|
||||
QJsonObject expose;
|
||||
expose["type"] = "enum";
|
||||
expose["property"] = "system_mode";
|
||||
expose["values"] = QJsonArray{"auto", "heat", "off"};
|
||||
|
||||
item.setFromExpose(expose);
|
||||
|
||||
QVERIFY2(item.getValueType() == ITEM_VALUE_ENUM, "Real device: ValueType should be ENUM");
|
||||
|
||||
auto names = item.getValueNames();
|
||||
QVERIFY2(names.size() == 3, "Real device: Should have 3 enum values");
|
||||
QVERIFY2(names[0] == "auto", "Real device: First value should be 'auto'");
|
||||
QVERIFY2(names[1] == "heat", "Real device: Second value should be 'heat'");
|
||||
QVERIFY2(names[2] == "off", "Real device: Third value should be 'off'");
|
||||
}
|
||||
|
||||
void cleanupTestCase()
|
||||
{
|
||||
// Cleanup after all tests
|
||||
|
|
@ -425,4 +185,4 @@ private slots:
|
|||
|
||||
QTEST_APPLESS_MAIN(TestMqttItem)
|
||||
|
||||
#include "test_mqttitem.moc"
|
||||
#include "test_mqttitem.moc"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue