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

@ -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"