Fix mqtt item expose detection only working on local instances

This commit is contained in:
Carl Philipp Klemm 2026-06-19 12:15:41 +02:00
parent 45676b3384
commit 58ba22b267
24 changed files with 340 additions and 415 deletions

View file

@ -6,9 +6,12 @@ project(smartvos VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Enable all warnings
add_compile_options(-Wall) add_compile_options(-Wall)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "/usr" CACHE PATH "..." FORCE)
endif()
# Find Qt packages # Find Qt packages
find_package(Qt6 COMPONENTS Core Gui Widgets Network Multimedia SerialPort Mqtt WebSockets REQUIRED) find_package(Qt6 COMPONENTS Core Gui Widgets Network Multimedia SerialPort Mqtt WebSockets REQUIRED)

View file

@ -14,8 +14,8 @@
#include <QJsonArray> #include <QJsonArray>
ItemData::ItemData(uint32_t itemIdIn, QString name, uint8_t value, bool loaded, bool hidden, item_value_type_t type, QString 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), loaded_(loaded), hidden_(hidden), type_(type), groupName_(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; return changes;
} }
bool ItemData::getLoaded() const
{
return loaded_;
}
void ItemData::setLoaded(bool loaded)
{
loaded_ = loaded;
}
bool ItemData::hasChanged(const ItemData& other) const bool ItemData::hasChanged(const ItemData& other) const
{ {
ItemFieldChanges changes(true); ItemFieldChanges changes(true);
@ -219,7 +209,7 @@ void ItemData::setOverride(bool overrideVal)
//item //item
Item::Item(uint32_t itemIdIn, QString name, uint8_t value, QObject *parent): QObject(parent), ItemData (itemIdIn, name, 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 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)
{
newItem->load(json); newItem->load(json);
newItem->setLoaded(true);
}
return newItem; return newItem;
} }

View file

@ -24,6 +24,26 @@ typedef enum {
ITEM_UPDATE_INVALID ITEM_UPDATE_INVALID
} item_update_type_t; } 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 ItemFieldChanges;
struct ItemUpdateRequest; struct ItemUpdateRequest;
@ -33,7 +53,6 @@ protected:
QString name_; QString name_;
uint8_t value_; uint8_t value_;
uint32_t itemId_; uint32_t itemId_;
bool loaded_;
bool hidden_; bool hidden_;
item_value_type_t type_; item_value_type_t type_;
QString groupName_; QString groupName_;
@ -44,7 +63,6 @@ public:
ItemData(uint32_t itemIdIn = QRandomGenerator::global()->generate(), ItemData(uint32_t itemIdIn = QRandomGenerator::global()->generate(),
QString name = "Item", QString name = "Item",
uint8_t value = 0, uint8_t value = 0,
bool loaded = false,
bool hidden = false, bool hidden = false,
item_value_type_t type = ITEM_VALUE_BOOL, item_value_type_t type = ITEM_VALUE_BOOL,
QString groupName = ""); QString groupName = "");
@ -65,8 +83,6 @@ public:
void setName(QString name); void setName(QString name);
uint8_t getValue() const; uint8_t getValue() const;
void setValueData(uint8_t value); void setValueData(uint8_t value);
bool getLoaded() const;
void setLoaded(bool loaded);
bool isHidden() const; bool isHidden() const;
void setHidden(bool hidden); void setHidden(bool hidden);
item_value_type_t getValueType(); item_value_type_t getValueType();

View file

@ -21,7 +21,6 @@ void ItemLoaderSource::refresh()
std::shared_ptr<Item> newItem = Item::loadItem(itemObject); std::shared_ptr<Item> newItem = Item::loadItem(itemObject);
if(newItem) if(newItem)
{ {
qDebug()<<"Loaded item"<<newItem->getName();
ItemAddRequest request; ItemAddRequest request;
request.type = ITEM_UPDATE_LOADED; request.type = ITEM_UPDATE_LOADED;
request.payload = newItem; request.payload = newItem;

View file

@ -23,7 +23,7 @@ void ItemStore::addItem(const ItemAddRequest& item)
{ {
items_.push_back(item.payload); items_.push_back(item.payload);
connect(item.payload.get(), &Item::updated, this, &ItemStore::itemUpdateSlot); 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())); itemAdded(std::weak_ptr<Item>(items_.back()));
} }
else if(!item.changes.isNone()) else if(!item.changes.isNone())

View file

@ -35,27 +35,12 @@ MqttItem::~MqttItem()
disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived); disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
workClient->unsubscribe(subscription); workClient->unsubscribe(subscription);
} }
if(devicesSubscription)
{
disconnect(devicesSubscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onDevicesMessageReceived);
workClient->unsubscribe(devicesSubscription);
}
} }
void MqttItem::onClientStateChanged(QMqttClient::ClientState state) void MqttItem::onClientStateChanged(QMqttClient::ClientState state)
{ {
if(state == QMqttClient::Connected) if(state == QMqttClient::Connected)
{
refreshSubscription(); 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() void MqttItem::refreshSubscription()
@ -77,16 +62,64 @@ void MqttItem::refreshSubscription()
connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived); connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
} }
void MqttItem::onDevicesMessageReceived(const QMqttMessage& message) bool MqttItem::setFromDevices(const QJsonArray& devices)
{ {
if(exposeLoaded_) QJsonObject device = getSelfFromDevices(devices);
return; if(device.empty())
return false;
return loadExposeFromDevice(device);
}
QJsonDocument doc = QJsonDocument::fromJson(message.payload()); std::vector<QString> MqttItem::getAvailableValueKeys(const QJsonArray& devices)
if(!doc.isArray()) {
return; QJsonObject device = getSelfFromDevices(devices);
if(device.empty())
return std::vector<QString>();
QJsonArray devices = doc.array(); 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)
{
for(const QJsonValue& deviceValue : devices) for(const QJsonValue& deviceValue : devices)
{ {
if(!deviceValue.isObject()) if(!deviceValue.isObject())
@ -98,32 +131,20 @@ void MqttItem::onDevicesMessageReceived(const QMqttMessage& message)
// Check if this device matches our topic (friendly_name) // Check if this device matches our topic (friendly_name)
QString friendlyName = device["friendly_name"].toString(); QString friendlyName = device["friendly_name"].toString();
if(friendlyName == topic_) if(friendlyName == topic_)
{ return device;
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;
}
} }
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 // Get definition - may be null for unsupported devices
QJsonObject definition = device["definition"].toObject(); QJsonObject definition = device["definition"].toObject();
if(definition.isEmpty()) if(definition.isEmpty())
{ {
qWarning() << "MqttItem" << topic_ << "device has no definition (unsupported)"; qWarning() << "MqttItem" << topic_ << "device has no definition (unsupported)";
return; return false;
} }
// Get exposes from definition // Get exposes from definition
@ -131,7 +152,7 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device)
if(exposes.isEmpty()) if(exposes.isEmpty())
{ {
qWarning() << "MqttItem" << topic_ << "device has no exposes"; qWarning() << "MqttItem" << topic_ << "device has no exposes";
return; return false;
} }
for(const QJsonValue& exposeValue : exposes) for(const QJsonValue& exposeValue : exposes)
@ -147,11 +168,11 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device)
{ {
setFromExpose(expose); setFromExpose(expose);
qDebug() << "MqttItem" << topic_ << "detected type" << expose["type"].toString() << "for property" << valueKey_; qDebug() << "MqttItem" << topic_ << "detected type" << expose["type"].toString() << "for property" << valueKey_;
return; return false;
} }
// Check if it's a composite type with features // 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(); QJsonArray features = expose["features"].toArray();
for(const QJsonValue& featureValue : features) for(const QJsonValue& featureValue : features)
@ -164,13 +185,14 @@ void MqttItem::loadExposeFromDevice(const QJsonObject& device)
{ {
setFromExpose(feature); setFromExpose(feature);
qDebug() << "MqttItem" << topic_ << "detected type" << feature["type"].toString() << "for property" << valueKey_; 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) void MqttItem::onMessageReceived(const QMqttMessage& message)
@ -294,30 +316,6 @@ void MqttItem::setFromExpose(const QJsonObject& expose)
hashId(); 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 QString MqttItem::getTopic() const
{ {
return topic_; return topic_;
@ -353,11 +351,6 @@ int MqttItem::getValueStep() const
return valueStep_; return valueStep_;
} }
bool MqttItem::getExposeLoaded() const
{
return exposeLoaded_;
}
void MqttItem::store(QJsonObject& json) void MqttItem::store(QJsonObject& json)
{ {
Item::store(json); Item::store(json);
@ -381,7 +374,6 @@ void MqttItem::load(const QJsonObject& json, const bool preserve)
valueMin_ = json["ValueMin"].toInt(0); valueMin_ = json["ValueMin"].toInt(0);
valueMax_ = json["ValueMax"].toInt(255); valueMax_ = json["ValueMax"].toInt(255);
valueStep_ = json["ValueStep"].toInt(1); valueStep_ = json["ValueStep"].toInt(1);
exposeLoaded_ = json["ExposeLoaded"].toBool(false);
hashId(); hashId();
refreshSubscription(); refreshSubscription();
} }

View file

@ -9,8 +9,6 @@ class QString;
class MqttItem : public Item class MqttItem : public Item
{ {
Q_OBJECT Q_OBJECT
Q_SIGNALS:
void exposeLoaded();
public: public:
inline static std::weak_ptr<MqttClient> client; inline static std::weak_ptr<MqttClient> client;
@ -23,17 +21,17 @@ private:
int valueMin_ = 0; int valueMin_ = 0;
int valueMax_ = 255; int valueMax_ = 255;
int valueStep_ = 1; int valueStep_ = 1;
bool exposeLoaded_ = false;
MqttClient::Subscription* subscription = nullptr; MqttClient::Subscription* subscription = nullptr;
MqttClient::Subscription* devicesSubscription = nullptr;
void hashId(); void hashId();
void refreshSubscription(); void refreshSubscription();
void onMessageReceived(const QMqttMessage& message); void onMessageReceived(const QMqttMessage& message);
void onClientStateChanged(QMqttClient::ClientState state); 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: public:
explicit MqttItem(QString name = "MqttItem", explicit MqttItem(QString name = "MqttItem",
@ -51,11 +49,8 @@ public:
void setValueStep(int step); void setValueStep(int step);
void setValueType(item_value_type_t type); void setValueType(item_value_type_t type);
// Configure from Zigbee2MQTT expose info bool setFromDevices(const QJsonArray& devices);
void setFromExpose(const QJsonObject& expose); std::vector<QString> getAvailableValueKeys(const QJsonArray& devices);
// Trigger expose lookup from bridge/devices
void triggerExposeLookup();
QString getTopic() const; QString getTopic() const;
QString getValueKey() const; QString getValueKey() const;
@ -64,7 +59,6 @@ public:
int getValueMin() const; int getValueMin() const;
int getValueMax() const; int getValueMax() const;
int getValueStep() const; int getValueStep() const;
bool getExposeLoaded() const;
virtual void store(QJsonObject& json) override; virtual void store(QJsonObject& json) override;
virtual void load(const QJsonObject& json, const bool preserve = false) override; virtual void load(const QJsonObject& json, const bool preserve = false) override;

View file

@ -8,6 +8,7 @@
#include "mqttclient.h" #include "mqttclient.h"
#include "items/mqttitem.h" #include "items/mqttitem.h"
#include "items/itemstore.h" #include "items/itemstore.h"
#include "ui/itemsettingswidgets/mqttitemsettingswidget.h"
MainObject::MainObject(QObject *parent) : MainObject::MainObject(QObject *parent) :
QObject(parent) QObject(parent)
@ -176,6 +177,8 @@ SecondaryMainObject::SecondaryMainObject(QString host, int port, QObject *parent
QMetaObject::invokeMethod(this, [](){exit(1);}, Qt::QueuedConnection); QMetaObject::invokeMethod(this, [](){exit(1);}, Qt::QueuedConnection);
} }
MqttItemSettingsWidget::service = tcpClient;
connect(&globalItems, &ItemStore::itemUpdated, tcpClient, &TcpClient::itemUpdated); connect(&globalItems, &ItemStore::itemUpdated, tcpClient, &TcpClient::itemUpdated);
globalItems.refresh(); globalItems.refresh();

View file

@ -114,7 +114,6 @@ Microcontroller::Microcontroller()
writeTimer.setInterval(50); writeTimer.setInterval(50);
writeTimer.setSingleShot(false); writeTimer.setSingleShot(false);
connect(&writeTimer, &QTimer::timeout, this, &Microcontroller::onWriteTimerTimeout); connect(&writeTimer, &QTimer::timeout, this, &Microcontroller::onWriteTimerTimeout);
qDebug()<<__func__<<writeTimer.isActive();
} }
Microcontroller::~Microcontroller() Microcontroller::~Microcontroller()
@ -139,10 +138,11 @@ std::shared_ptr<Relay> Microcontroller::processRelayLine(const QString& buffer)
name.remove(name.size()-1, 1); name.remove(name.size()-1, 1);
else else
name = "Relay " + QString::number(bufferList[1].toInt(nullptr, 2)); 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, name,
bufferList[6].toInt(nullptr, 2), bufferList[4].toInt(nullptr, 2),
bufferList[8].toInt())); bufferList[8].toInt()));
return relay;
} }
return nullptr; return nullptr;
} }

View file

@ -26,6 +26,20 @@ void MqttClient::start(const QJsonObject& settings)
client->connectToHost(); 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) void MqttClient::onClientError(QMqttClient::ClientError error)
{ {
qWarning()<<"MQTT Client error:"<<error; qWarning()<<"MQTT Client error:"<<error;
@ -34,11 +48,19 @@ void MqttClient::onClientError(QMqttClient::ClientError error)
void MqttClient::onClientStateChanged(QMqttClient::ClientState state) void MqttClient::onClientStateChanged(QMqttClient::ClientState state)
{ {
if(state == QMqttClient::ClientState::Connected) 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(); qInfo()<<"Connected to MQTT broker at "<<client->hostname()<<client->port();
}
else if (state == QMqttClient::ClientState::Disconnected) else if (state == QMqttClient::ClientState::Disconnected)
{
qWarning()<<"Lost connection to MQTT broker"; qWarning()<<"Lost connection to MQTT broker";
}
else if(state == QMqttClient::ClientState::Connecting) else if(state == QMqttClient::ClientState::Connecting)
{
qInfo()<<"Connecting to MQTT broker at "<<client->hostname()<<client->port(); qInfo()<<"Connecting to MQTT broker at "<<client->hostname()<<client->port();
}
} }
void MqttClient::store(QJsonObject& json) void MqttClient::store(QJsonObject& json)
@ -107,6 +129,8 @@ QString MqttClient::getBaseTopic()
MqttClient::~MqttClient() MqttClient::~MqttClient()
{ {
if(devicesSubscription)
unsubscribe(devicesSubscription);
for(const std::pair<QString, Subscription*> sub : subscriptions) for(const std::pair<QString, Subscription*> sub : subscriptions)
{ {
qWarning()<<sub.first<<"not unregistered at exit!"; qWarning()<<sub.first<<"not unregistered at exit!";

View file

@ -4,6 +4,7 @@
#include <QMqttClient> #include <QMqttClient>
#include <QObject> #include <QObject>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray>
#include <map> #include <map>
class MqttClient: public QObject class MqttClient: public QObject
@ -20,10 +21,13 @@ private:
QString baseTopicName; QString baseTopicName;
std::shared_ptr<QMqttClient> client; std::shared_ptr<QMqttClient> client;
std::map<QString, Subscription*> subscriptions; std::map<QString, Subscription*> subscriptions;
Subscription* devicesSubscription = nullptr;
QJsonArray devices;
private slots: private slots:
void onClientStateChanged(QMqttClient::ClientState state); void onClientStateChanged(QMqttClient::ClientState state);
void onClientError(QMqttClient::ClientError error); void onClientError(QMqttClient::ClientError error);
void onDevicesMessageReceived(const QMqttMessage& message);
public: public:
explicit MqttClient(); explicit MqttClient();
@ -35,6 +39,7 @@ public:
void unsubscribe(Subscription* subscription); void unsubscribe(Subscription* subscription);
void unsubscribe(QString topic); void unsubscribe(QString topic);
QString getBaseTopic(); QString getBaseTopic();
QJsonArray getDevicesArray();
}; };
#endif // MQTTCLIENT_H #endif // MQTTCLIENT_H

View file

@ -5,6 +5,32 @@
SensorStore globalSensors; 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) SensorStore::SensorStore(QObject *parent): QObject(parent)
{ {
} }
@ -12,7 +38,13 @@ SensorStore::SensorStore(QObject *parent): QObject(parent)
void SensorStore::store(QJsonObject& json) void SensorStore::store(QJsonObject& json)
{ {
QJsonArray sensorsArray; 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; QJsonObject sensorObject;
sensor.store(sensorObject); sensor.store(sensorObject);

View file

@ -176,6 +176,9 @@ typedef enum {
SENSOR_UPDATE_INVALID SENSOR_UPDATE_INVALID
} sensor_update_type_t; } sensor_update_type_t;
QString sensorUpdateTypeToString(sensor_update_type_t type);
sensor_update_type_t sensorUpdateTypeFromString(const QString& string);
class SensorStore: public QObject class SensorStore: public QObject
{ {
Q_OBJECT Q_OBJECT

View file

@ -3,6 +3,7 @@
#include <QJsonArray> #include <QJsonArray>
#include "items/item.h" #include "items/item.h"
#include "items/mqttitem.h"
#include "service.h" #include "service.h"
#include "server.h" #include "server.h"
@ -33,7 +34,6 @@ void Server::processIncomeingJson(const QByteArray& jsonbytes)
if(item) if(item)
{ {
qDebug()<<"Server got item"<<item->getName(); qDebug()<<"Server got item"<<item->getName();
item->setLoaded(FullList);
fieldChanges.push_back(item->loadWithChanges(jsonobject)); fieldChanges.push_back(item->loadWithChanges(jsonobject));
items.push_back(item); items.push_back(item);
} }
@ -57,12 +57,24 @@ void Server::processIncomeingJson(const QByteArray& jsonbytes)
updateItems(updates); updateItems(updates);
} }
} }
else if(type == "GetMqttDevices")
{
sendMqttDevices();
}
else else
{ {
Service::processIncomeingJson(jsonbytes); 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() void Server::handleSocketError()
{ {
QObject* obj = sender(); QObject* obj = sender();

View file

@ -44,6 +44,7 @@ protected:
void handleSocketDisconnect(); void handleSocketDisconnect();
void removeClient(QTcpSocket* socket); void removeClient(QTcpSocket* socket);
void removeClient(QWebSocket* socket); void removeClient(QWebSocket* socket);
void sendMqttDevices();
signals: signals:
void sigRequestSave(); void sigRequestSave();

View file

@ -48,6 +48,12 @@ void Service::refresh()
sendJson(createMessage("GetItems", QJsonArray())); sendJson(createMessage("GetItems", QJsonArray()));
} }
void Service::requestMqttDevices()
{
qDebug()<<__func__;
sendJson(createMessage("GetMqttDevices", QJsonArray()));
}
void Service::sendSensors() void Service::sendSensors()
{ {
QJsonArray sensors; QJsonArray sensors;
@ -96,7 +102,17 @@ void Service::processIncomeingJson(const QByteArray& jsonbytes)
{ {
QJsonObject jsonobject = sensorjson.toObject(); QJsonObject jsonobject = sensorjson.toObject();
Sensor sensor(jsonobject); 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") else if(type == "AddSensor")
@ -107,4 +123,10 @@ void Service::processIncomeingJson(const QByteArray& jsonbytes)
QJsonObject payload = json["Payload"].toObject(); QJsonObject payload = json["Payload"].toObject();
emit sensorAdded(sensor, backend, payload); emit sensorAdded(sensor, backend, payload);
} }
else if(type == "MqttDevices")
{
qDebug()<<"Got mqtt devices";
QJsonArray payload = json["Data"].toArray();
gotMqttDevices(payload);
}
} }

View file

@ -22,12 +22,14 @@ 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); void sensorAdded(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload);
void gotMqttDevices(QJsonArray devices);
public slots: public slots:
virtual void sensorEvent(Sensor sensor, sensor_update_type_t type); virtual 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 = {}); virtual void addSensor(Sensor sensor, Sensor::sensor_backend_type_t backend, QJsonObject payload = {});
void requestMqttDevices();
public: public:
Service(QObject* parent = nullptr); Service(QObject* parent = nullptr);

View file

@ -48,7 +48,6 @@ void TcpClient::processIncomeingJson(const QByteArray& jsonbytes)
std::shared_ptr<Item> item = Item::loadItem(jsonobject); std::shared_ptr<Item> item = Item::loadItem(jsonobject);
if(item) if(item)
{ {
item->setLoaded(FullList);
fieldChanges.push_back(item->loadWithChanges(jsonobject)); fieldChanges.push_back(item->loadWithChanges(jsonobject));
items.push_back(item); items.push_back(item);
} }

View file

@ -1,7 +1,9 @@
#include "mqttitemsettingswidget.h" #include "mqttitemsettingswidget.h"
#include "ui_mqttitemsettingswidget.h" #include "ui_mqttitemsettingswidget.h"
#include "programmode.h"
#include <QInputDialog> #include <QInputDialog>
#include <QMessageBox>
#include <QDebug> #include <QDebug>
MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent) : MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent) :
@ -39,43 +41,6 @@ MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWi
updateValueNamesFromItem(); updateValueNamesFromItem();
suppressUpdates_ = false; suppressUpdates_ = false;
updateVisibility(); 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 // 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_addValueName, &QPushButton::clicked, this, &MqttItemSettingsWidget::onAddValueName);
connect(ui->pushButton_removeValueName, &QPushButton::clicked, this, &MqttItemSettingsWidget::onRemoveValueName); connect(ui->pushButton_removeValueName, &QPushButton::clicked, this, &MqttItemSettingsWidget::onRemoveValueName);
connect(ui->listWidget_valueNames, &QListWidget::itemChanged, this, &MqttItemSettingsWidget::onValueNamesChanged); 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) void MqttItemSettingsWidget::setTopic(const QString& topic)
@ -186,7 +205,24 @@ void MqttItemSettingsWidget::onAutoDetectClicked()
if(auto workingItem = item_.lock()) if(auto workingItem = item_.lock())
{ {
ui->label_status->setText("Detecting..."); 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();
}
} }
} }

View file

@ -4,6 +4,7 @@
#include <QWidget> #include <QWidget>
#include <memory> #include <memory>
#include "../../items/mqttitem.h" #include "../../items/mqttitem.h"
#include "../../service/service.h"
namespace Ui namespace Ui
{ {
@ -13,9 +14,17 @@ class MqttItemSettingsWidget;
class MqttItemSettingsWidget : public QWidget class MqttItemSettingsWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
public:
inline static Service* service = nullptr;
private:
std::weak_ptr<MqttItem> item_; std::weak_ptr<MqttItem> item_;
bool suppressUpdates_ = false; bool suppressUpdates_ = false;
void exposeLoaded();
private slots: private slots:
void setTopic(const QString& topic); void setTopic(const QString& topic);
void setValueKey(const QString& valueKey); void setValueKey(const QString& valueKey);
@ -29,6 +38,7 @@ private slots:
void onAddValueName(); void onAddValueName();
void onRemoveValueName(); void onRemoveValueName();
void onValueNamesChanged(); void onValueNamesChanged();
void onGotMqttDevices(QJsonArray devices);
public: public:
explicit MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent = nullptr); explicit MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent = nullptr);

View file

@ -15,6 +15,8 @@ ItemWidget::ItemWidget(std::weak_ptr<Item> item, bool noGroupEdit, QWidget *pare
{ {
ui->setupUi(this); ui->setupUi(this);
connect(&valueTimer, &QTimer::timeout, this, &ItemWidget::onValueTimerTimeout);
if(auto workingItem = item_.lock()) if(auto workingItem = item_.lock())
{ {
if(workingItem->getValueType() == ITEM_VALUE_UINT) if(workingItem->getValueType() == ITEM_VALUE_UINT)
@ -76,6 +78,21 @@ void ItemWidget::deleteItem()
} }
void ItemWidget::moveToValue(int value) 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()) 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() void ItemWidget::disable()

View file

@ -2,6 +2,8 @@
#define RELAYWIDGET_H #define RELAYWIDGET_H
#include <QWidget> #include <QWidget>
#include <QQueue>
#include <QTimer>
#include <memory> #include <memory>
#include "../items/item.h" #include "../items/item.h"
@ -17,6 +19,9 @@ private:
std::weak_ptr<Item> item_; std::weak_ptr<Item> item_;
bool noGroupEdit_; bool noGroupEdit_;
QQueue<int> valueQue;
QTimer valueTimer;
void disable(); void disable();
signals: signals:
@ -28,6 +33,7 @@ private slots:
void moveToState(bool state); void moveToState(bool state);
void moveToValue(int value); void moveToValue(int value);
void deleteItem(); void deleteItem();
void onValueTimerTimeout();
public: public:
explicit ItemWidget(std::weak_ptr<Item> item, bool noGroupEdit = false, QWidget *parent = nullptr); explicit ItemWidget(std::weak_ptr<Item> item, bool noGroupEdit = false, QWidget *parent = nullptr);
@ -41,6 +47,7 @@ public slots:
void onItemUpdated(ItemUpdateRequest update); void onItemUpdated(ItemUpdateRequest update);
private: private:
void applyValue(int value);
Ui::ItemWidget *ui; Ui::ItemWidget *ui;
}; };

View file

@ -73,15 +73,6 @@ private slots:
QVERIFY(!item.isHidden()); QVERIFY(!item.isHidden());
} }
void testItemLoaded()
{
Item item(1, "test_item", 0);
QVERIFY(!item.getLoaded());
item.setLoaded(true);
QVERIFY(item.getLoaded());
}
void testItemJsonSerialization() void testItemJsonSerialization()
{ {
Item item(42, "test_item", 1); Item item(42, "test_item", 1);

View file

@ -138,66 +138,6 @@ private slots:
QVERIFY(item.indexToValueName(99).isEmpty()); // Out of bounds 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() void testJsonSerialization()
{ {
MqttItem item("test_mqtt", 1); MqttItem item("test_mqtt", 1);
@ -237,186 +177,6 @@ private slots:
QVERIFY(item.getValueOff() == "OFF"); 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() void cleanupTestCase()
{ {
// Cleanup after all tests // Cleanup after all tests