diff --git a/libnymea/integrations/pluginmetadata.cpp b/libnymea/integrations/pluginmetadata.cpp index 0101d1f8..17b7269d 100644 --- a/libnymea/integrations/pluginmetadata.cpp +++ b/libnymea/integrations/pluginmetadata.cpp @@ -27,26 +27,26 @@ #include "types/interface.h" -#include #include #include -#include +#include #include -#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include #endif -PluginMetadata::PluginMetadata() -{ +static const QList s_validTypesForStepSize + = {QMetaType::Int, QMetaType::UInt, QMetaType::LongLong, QMetaType::ULongLong, QMetaType::Double, QMetaType::Float, QMetaType::Short, QMetaType::ULong, QMetaType::UShort}; -} +PluginMetadata::PluginMetadata() {} -PluginMetadata::PluginMetadata(const QJsonObject &jsonObject, bool isBuiltIn, bool strict): - m_jsonObject(jsonObject), - m_isBuiltIn(isBuiltIn), - m_strictRun(strict) +PluginMetadata::PluginMetadata(const QJsonObject &jsonObject, bool isBuiltIn, bool strict) + : m_jsonObject(jsonObject) + , m_isBuiltIn(isBuiltIn) + , m_strictRun(strict) { -#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) qRegisterMetaType("QColor"); #endif @@ -165,7 +165,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check mandatory fields if (!verificationResult.first.isEmpty()) { - m_validationErrors.append("Vendor metadata has missing fields: " + verificationResult.first.join(", ") + "\n" + qUtf8Printable(QJsonDocument::fromVariant(vendorObject.toVariantMap()).toJson(QJsonDocument::Indented))); + m_validationErrors.append("Vendor metadata has missing fields: " + verificationResult.first.join(", ") + "\n" + + qUtf8Printable(QJsonDocument::fromVariant(vendorObject.toVariantMap()).toJson(QJsonDocument::Indented))); hasError = true; // Not continuing parsing vendor as we rely on mandatory fields being around. break; @@ -176,7 +177,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check if there are any unknown fields if (!verificationResult.second.isEmpty()) { - m_validationErrors.append("Vendor \"" + vendorName + "\" has unknown fields: \"" + verificationResult.second.join("\", \"") + "\"\n" + qUtf8Printable(QJsonDocument::fromVariant(vendorObject.toVariantMap()).toJson(QJsonDocument::Indented))); + m_validationErrors.append("Vendor \"" + vendorName + "\" has unknown fields: \"" + verificationResult.second.join("\", \"") + "\"\n" + + qUtf8Printable(QJsonDocument::fromVariant(vendorObject.toVariantMap()).toJson(QJsonDocument::Indented))); hasError = true; } @@ -194,15 +196,26 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Load thing classes of this vendor foreach (const QJsonValue &thingClassJson, vendorJson.toObject().value("thingClasses").toArray()) { - // FIXME: Drop this when possible, see .h for context m_currentScopUuids.clear(); QJsonObject thingClassObject = thingClassJson.toObject(); /*! Returns a list of all valid JSON properties a ThingClass JSON definition can have. */ - QStringList thingClassProperties = QStringList() << "id" << "name" << "displayName" << "createMethods" << "setupMethod" - << "interfaces" << "providedInterfaces" << "browsable" << "discoveryParamTypes" - << "paramTypes" << "settingsTypes" << "stateTypes" << "actionTypes" << "eventTypes" << "browserItemActionTypes" + QStringList thingClassProperties = QStringList() << "id" + << "name" + << "displayName" + << "createMethods" + << "setupMethod" + << "interfaces" + << "providedInterfaces" + << "browsable" + << "discoveryParamTypes" + << "paramTypes" + << "settingsTypes" + << "stateTypes" + << "actionTypes" + << "eventTypes" + << "browserItemActionTypes" << "discoveryType"; QStringList mandatoryThingClassProperties = QStringList() << "id" << "name" << "displayName"; @@ -210,7 +223,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check mandatory fields if (!verificationResult.first.isEmpty()) { - m_validationErrors.append("Thing class has missing fields: \"" + verificationResult.first.join("\", \"") + "\"\n" + qUtf8Printable(QJsonDocument::fromVariant(thingClassObject.toVariantMap()).toJson(QJsonDocument::Indented))); + m_validationErrors.append("Thing class has missing fields: \"" + verificationResult.first.join("\", \"") + "\"\n" + + qUtf8Printable(QJsonDocument::fromVariant(thingClassObject.toVariantMap()).toJson(QJsonDocument::Indented))); hasError = true; // Stop parsing this thingClass as we rely on mandatory fields being around. continue; @@ -221,7 +235,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check if there are any unknown fields if (!verificationResult.second.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClassName + "\" has unknown fields: \"" + verificationResult.second.join("\", \"") + "\"\n" + qUtf8Printable(QJsonDocument::fromVariant(thingClassObject.toVariantMap()).toJson(QJsonDocument::Indented))); + m_validationErrors.append("Thing class \"" + thingClassName + "\" has unknown fields: \"" + verificationResult.second.join("\", \"") + "\"\n" + + qUtf8Printable(QJsonDocument::fromVariant(thingClassObject.toVariantMap()).toJson(QJsonDocument::Indented))); hasError = true; } @@ -253,7 +268,7 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) } else if (createMethodValue.toString().toLower() == "user") { createMethods |= ThingClass::CreateMethodUser; } else { - m_validationErrors.append("Unknown createMehtod \"" + createMethodValue.toString() + "\" in thingClass \"" + thingClass.name() + "\"."); + m_validationErrors.append("Unknown createMehtod \"" + createMethodValue.toString() + "\" in thingClass \"" + thingClass.name() + "\"."); hasError = true; } } @@ -267,7 +282,7 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) } else if (discoveryTypeString == "weak") { thingClass.setDiscoveryType(ThingClass::DiscoveryTypeWeak); } else { - m_validationErrors.append("Unknown discoveryType \"" + discoveryTypeString + "\" in thingClass \"" + thingClass.name() + "\"."); + m_validationErrors.append("Unknown discoveryType \"" + discoveryTypeString + "\" in thingClass \"" + thingClass.name() + "\"."); hasError = true; } } else { @@ -333,15 +348,31 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) bool writableState = false; // TODO: DEPRECATED 1.2: Remove displayNameEvent eventually (requires updating all plugins) - QStringList stateTypeProperties = {"id", "name", "displayName", "displayNameEvent", "type", "defaultValue", "cached", - "unit", "minValue", "maxValue", "possibleValues", "writable", "displayNameAction", - "ioType", "suggestLogging", "filter"}; + QStringList stateTypeProperties = {"id", + "name", + "displayName", + "displayNameEvent", + "type", + "defaultValue", + "cached", + "unit", + "minValue", + "maxValue", + "stepSize", + "possibleValues", + "writable", + "displayNameAction", + "ioType", + "suggestLogging", + "filter"}; + QStringList mandatoryStateTypeProperties = {"id", "name", "displayName", "type", "defaultValue"}; QPair verificationResult = verifyFields(stateTypeProperties, mandatoryStateTypeProperties, st); // Check mandatory fields if (!verificationResult.first.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" has missing properties \"" + verificationResult.first.join("\", \"") + "\" in stateType definition\n" + qUtf8Printable(QJsonDocument::fromVariant(st.toVariantMap()).toJson(QJsonDocument::Indented))); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" has missing properties \"" + verificationResult.first.join("\", \"") + + "\" in stateType definition\n" + qUtf8Printable(QJsonDocument::fromVariant(st.toVariantMap()).toJson(QJsonDocument::Indented))); hasError = true; // Not processing further as mandatory fields are expected to be here continue; @@ -352,7 +383,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check if there are any unknown fields if (!verificationResult.second.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has unknown properties \"" + verificationResult.second.join("\", \"") + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has unknown properties \"" + + verificationResult.second.join("\", \"") + "\""); hasError = true; } // Print warning on deprecated fields @@ -365,17 +397,19 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) if (st.contains("writable") && st.value("writable").toBool()) { writableState = true; if (!st.contains("displayNameAction")) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has writable state but does not define the displayNameAction property"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" has writable state but does not define the displayNameAction property"); hasError = true; } } -#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QMetaType::Type t = static_cast(QMetaType::fromName(QByteArray(st.value("type").toString().toUtf8())).id()); #else QMetaType::Type t = static_cast(QVariant::nameToType(st.value("type").toString().toLatin1().data())); #endif if (t == QMetaType::UnknownType) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid type: \"" + st.value("type").toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid type: \"" + st.value("type").toString() + + "\""); hasError = true; } @@ -417,6 +451,23 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) stateType.setMaxValue(maxValue); } + if (st.contains("stepSize")) { + double stepSize = st.value("stepSize").toDouble(); + if (stepSize != 0) { + if (!s_validTypesForStepSize.contains(stateType.type())) { + m_validationErrors.append("Thing \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has configured a step size but the data type " + + st.value("type").toString() + " does not support that. Only numeric state types can have a stepSize."); + hasError = true; + } else if (stepSize < 0) { + m_validationErrors.append("Thing \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" has configured a negative stepSize which is not allowed."); + hasError = true; + } else { + stateType.setStepSize(stepSize); + } + } + } + if (st.contains("possibleValues")) { QVariantList possibleValues; QStringList possibleValuesDisplayNames; @@ -427,7 +478,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) if (possibleValueJson.isObject()) { if (!possibleValue.toMap().contains("value") || !possibleValue.toMap().contains("displayName")) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid possible value \"" + possibleValueJson.toString() + "\" which is of object type but does not have \"value\" and \"displayName\" properties."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid possible value \"" + + possibleValueJson.toString() + "\" which is of object type but does not have \"value\" and \"displayName\" properties."); hasError = true; break; } @@ -445,7 +497,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) stateType.setPossibleValuesDisplayNames(possibleValuesDisplayNames); if (!stateType.possibleValues().contains(stateType.defaultValue())) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid default value \"" + stateType.defaultValue().toString() + "\" which is not in the list of possible values."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid default value \"" + + stateType.defaultValue().toString() + "\" which is not in the list of possible values."); hasError = true; break; } @@ -463,54 +516,64 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) Types::IOType ioType = Types::IOTypeNone; if (ioTypeString == "digitalInput") { if (stateType.type() != QMetaType::Bool) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital input but type is not \"bool\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" is marked as digital input but type is not \"bool\""); hasError = true; break; } ioType = Types::IOTypeDigitalInput; } else if (ioTypeString == "digitalOutput") { if (stateType.type() != QMetaType::Bool) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital output but type is not \"bool\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" is marked as digital output but type is not \"bool\""); hasError = true; break; } if (!stateType.writable()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital output but is not writable"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" is marked as digital output but is not writable"); hasError = true; break; } ioType = Types::IOTypeDigitalOutput; } else if (ioTypeString == "analogInput") { if (stateType.type() != QMetaType::Double && stateType.type() != QMetaType::Int && stateType.type() != QMetaType::UInt) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog input but type is not \"double\", \"int\" or \"uint\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" is marked as analog input but type is not \"double\", \"int\" or \"uint\""); hasError = true; break; } if (stateType.minValue().isNull() || stateType.maxValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog input but it does not define \"minValue\" and \"maxValue\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" is marked as analog input but it does not define \"minValue\" and \"maxValue\""); hasError = true; break; } ioType = Types::IOTypeAnalogInput; } else if (ioTypeString == "analogOutput") { if (stateType.type() != QMetaType::Double && stateType.type() != QMetaType::Int && stateType.type() != QMetaType::UInt) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but type is not \"double\", \"int\" or \"uint\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" is marked as analog output but type is not \"double\", \"int\" or \"uint\""); hasError = true; break; } if (!stateType.writable()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but is not writable"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" is marked as analog output but is not writable"); hasError = true; break; } if (stateType.minValue().isNull() || stateType.maxValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but it does not define \"minValue\" and \"maxValue\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" is marked as analog output but it does not define \"minValue\" and \"maxValue\""); hasError = true; break; } ioType = Types::IOTypeAnalogOutput; } else { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid ioType value \"IOTypeNone\" which is not any of \"digitalInput\", \"digitalOutput\", \"analogInput\" or \"analogOutput\""); + m_validationErrors.append( + "Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + + "\" has invalid ioType value \"IOTypeNone\" which is not any of \"digitalInput\", \"digitalOutput\", \"analogInput\" or \"analogOutput\""); hasError = true; break; } @@ -524,7 +587,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) if (filter == "adaptive") { stateType.setFilter(Types::StateValueFilterAdaptive); } else if (!filter.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid filter value \"" + filter + "\". Supported filters are: \"adaptive\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid filter value \"" + filter + + "\". Supported filters are: \"adaptive\""); hasError = true; } } @@ -538,6 +602,7 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) paramType.setDefaultValue(stateType.defaultValue()); paramType.setMinValue(stateType.minValue()); paramType.setMaxValue(stateType.maxValue()); + paramType.setStepSize(stateType.stepSize()); paramType.setUnit(stateType.unit()); ActionType actionType(ActionTypeId(stateType.id().toString())); @@ -560,7 +625,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check mandatory fields if (!verificationResult.first.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" has missing fields \"" + verificationResult.first.join("\", \"") + "\" in action type definition."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" has missing fields \"" + verificationResult.first.join("\", \"") + + "\" in action type definition."); hasError = true; continue; } @@ -570,7 +636,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check if there are any unknown fields if (!verificationResult.second.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" action type \"" + actionTypeName + "\" has unknown fields \"" + verificationResult.second.join("\", \"") + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" action type \"" + actionTypeName + "\" has unknown fields \"" + + verificationResult.second.join("\", \"") + "\""); hasError = true; } @@ -610,7 +677,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check mandatory fields if (!verificationResult.first.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" has missing fields \"" + verificationResult.first.join("\", \"") + "\" in event type defintion"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" has missing fields \"" + verificationResult.first.join("\", \"") + + "\" in event type defintion"); hasError = true; continue; } @@ -620,7 +688,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check if there are any unknown fields if (!verificationResult.second.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" event type \"" + eventTypeName + "\" has unknown fields \"" + verificationResult.second.join("\", \"") + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" event type \"" + eventTypeName + "\" has unknown fields \"" + + verificationResult.second.join("\", \"") + "\""); hasError = true; } @@ -657,7 +726,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check mandatory fields if (!verificationResult.first.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" has missing fields \"" + verificationResult.first.join("\", \"") + "\" in browser item action type definition"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" has missing fields \"" + verificationResult.first.join("\", \"") + + "\" in browser item action type definition"); hasError = true; continue; } @@ -667,16 +737,19 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // Check if there are any unknown fields if (!verificationResult.second.isEmpty()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" browser action type \"" + actionTypeName + "\" has unknown fields \"" + verificationResult.first.join("\", \"") + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" browser action type \"" + actionTypeName + "\" has unknown fields \"" + + verificationResult.first.join("\", \"") + "\""); hasError = true; } if (actionTypeId.isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" browser action type \"" + actionTypeName + "\" has invalid UUID: " + at.value("id").toString()); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" browser action type \"" + actionTypeName + + "\" has invalid UUID: " + at.value("id").toString()); hasError = true; } if (!verifyDuplicateUuid(actionTypeId)) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" browser action type \"" + actionTypeName + "\" has duplicate UUID: " + actionTypeId.toString()); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" browser action type \"" + actionTypeName + + "\" has duplicate UUID: " + actionTypeId.toString()); hasError = true; } ActionType actionType(actionTypeId); @@ -708,105 +781,117 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) foreach (const InterfaceParamType &ifaceParamType, iface.paramTypes()) { if (!thingClass.paramTypes().contains(ifaceParamType.name())) { if (!ifaceParamType.optional()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + - "\" but doesn't implement param \"" + ifaceParamType.name() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + + "\" but doesn't implement param \"" + ifaceParamType.name() + "\""); hasError = true; } continue; } ParamType ¶mType = thingClass.paramTypes()[ifaceParamType.name()]; if (ifaceParamType.type() != paramType.type()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + - "\" but param \"" + paramType.name() + "\" has not matching type: \"" + - QVariant::typeToName(paramType.type()) + "\" != \"" + QVariant::typeToName(ifaceParamType.type()) + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + + paramType.name() + "\" has not matching type: \"" + QVariant::typeToName(paramType.type()) + "\" != \"" + + QVariant::typeToName(ifaceParamType.type()) + "\""); hasError = true; } if (ifaceParamType.minValue().isValid() && !ifaceParamType.minValue().isNull()) { if (ifaceParamType.minValue().toString() == "any") { if (paramType.minValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + - "\" but param \"" + paramType.name() + "\" has no minimum value defined."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + + paramType.name() + "\" has no minimum value defined."); hasError = true; } } else if (ifaceParamType.minValue() != paramType.minValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + - "\" but param \"" + paramType.name() + "\" has not matching minimum value: \"" + - ifaceParamType.minValue().toString() + "\" != \"" + paramType.minValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + + paramType.name() + "\" has not matching minimum value: \"" + ifaceParamType.minValue().toString() + "\" != \"" + + paramType.minValue().toString() + "\""); hasError = true; } } if (ifaceParamType.maxValue().isValid() && !ifaceParamType.maxValue().isNull()) { if (ifaceParamType.maxValue().toString() == "any") { if (paramType.maxValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + - "\" but param \"" + paramType.name() + "\" has no maximum value defined."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + + paramType.name() + "\" has no maximum value defined."); hasError = true; } } else if (ifaceParamType.maxValue() != paramType.maxValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + - "\" but param \"" + paramType.name() + "\" has not matching maximum value: \"" + - ifaceParamType.maxValue().toString() + "\" != \"" + paramType.minValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + + paramType.name() + "\" has not matching maximum value: \"" + ifaceParamType.maxValue().toString() + "\" != \"" + + paramType.minValue().toString() + "\""); hasError = true; } } if (!ifaceParamType.allowedValues().isEmpty() && ifaceParamType.allowedValues() != paramType.allowedValues()) { qCritical() << ifaceParamType.allowedValues(); - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + - paramType.name() + "\" has not matching allowed values."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + + paramType.name() + "\" has not matching allowed values."); hasError = true; } if (ifaceParamType.unit() != Types::UnitNone && ifaceParamType.unit() != paramType.unit()) { QMetaEnum unitEnum = QMetaEnum::fromType(); - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + - paramType.name() + "\" has not matching unit: \"" + unitEnum.valueToKey(ifaceParamType.unit()) + "\" != \"" + unitEnum.valueToKey(paramType.unit())); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but param \"" + + paramType.name() + "\" has not matching unit: \"" + unitEnum.valueToKey(ifaceParamType.unit()) + "\" != \"" + + unitEnum.valueToKey(paramType.unit())); hasError = true; } } - foreach (const InterfaceStateType &ifaceStateType, iface.stateTypes()) { if (!stateTypes.contains(ifaceStateType.name())) { if (!ifaceStateType.optional()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but doesn't implement state \"" + ifaceStateType.name() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + + "\" but doesn't implement state \"" + ifaceStateType.name() + "\""); hasError = true; } continue; } StateType &stateType = stateTypes[ifaceStateType.name()]; if (ifaceStateType.type() != stateType.type()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has not matching type: \"" + QVariant::typeToName(stateType.type()) + "\" != \"" + QVariant::typeToName(ifaceStateType.type()) + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + + stateType.name() + "\" has not matching type: \"" + QVariant::typeToName(stateType.type()) + "\" != \"" + + QVariant::typeToName(ifaceStateType.type()) + "\""); hasError = true; } if (ifaceStateType.minValue().isValid() && !ifaceStateType.minValue().isNull()) { if (ifaceStateType.minValue().toString() == "any") { if (stateType.minValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has no minimum value defined."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + + stateType.name() + "\" has no minimum value defined."); hasError = true; } } else if (ifaceStateType.minValue() != stateType.minValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has not matching minimum value: \"" + ifaceStateType.minValue().toString() + "\" != \"" + stateType.minValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + + stateType.name() + "\" has not matching minimum value: \"" + ifaceStateType.minValue().toString() + "\" != \"" + + stateType.minValue().toString() + "\""); hasError = true; } } if (ifaceStateType.maxValue().isValid() && !ifaceStateType.maxValue().isNull()) { if (ifaceStateType.maxValue().toString() == "any") { if (stateType.maxValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has no maximum value defined."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + + stateType.name() + "\" has no maximum value defined."); hasError = true; } } else if (ifaceStateType.maxValue() != stateType.maxValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has not matching maximum value: \"" + ifaceStateType.maxValue().toString() + "\" != \"" + stateType.minValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + + stateType.name() + "\" has not matching maximum value: \"" + ifaceStateType.maxValue().toString() + "\" != \"" + + stateType.minValue().toString() + "\""); hasError = true; } } if (!ifaceStateType.possibleValues().isEmpty() && ifaceStateType.possibleValues() != stateType.possibleValues()) { qCritical() << ifaceStateType.possibleValues(); - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has not matching allowed values."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + + stateType.name() + "\" has not matching allowed values."); hasError = true; } if (ifaceStateType.unit() != Types::UnitNone && ifaceStateType.unit() != stateType.unit()) { QMetaEnum unitEnum = QMetaEnum::fromType(); - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has not matching unit: \"" + unitEnum.valueToKey(ifaceStateType.unit()) + "\" != \"" + unitEnum.valueToKey(stateType.unit())); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + + stateType.name() + "\" has not matching unit: \"" + unitEnum.valueToKey(ifaceStateType.unit()) + "\" != \"" + + unitEnum.valueToKey(stateType.unit())); hasError = true; } @@ -819,7 +904,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) foreach (const InterfaceActionType &ifaceActionType, iface.actionTypes()) { if (!actionTypes.contains(ifaceActionType.name())) { if (!ifaceActionType.optional()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but doesn't implement action \"" + ifaceActionType.name() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + + "\" but doesn't implement action \"" + ifaceActionType.name() + "\""); hasError = true; } continue; @@ -829,49 +915,63 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) foreach (const ParamType &ifaceActionParamType, ifaceActionType.paramTypes()) { ParamType paramType = actionType.paramTypes().findByName(ifaceActionParamType.name()); if (!paramType.isValid()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" doesn't implement action param \"" + ifaceActionParamType.name() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" doesn't implement action param \"" + ifaceActionParamType.name() + "\""); hasError = true; } else { if (paramType.type() != ifaceActionParamType.type()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" is of wrong type: \"" + QVariant::typeToName(paramType.type()) + "\" expected: \"" + QVariant::typeToName(ifaceActionParamType.type()) + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + "\" is of wrong type: \"" + + QVariant::typeToName(paramType.type()) + "\" expected: \"" + QVariant::typeToName(ifaceActionParamType.type()) + "\""); hasError = true; } foreach (const QVariant &allowedValue, ifaceActionParamType.allowedValues()) { if (!paramType.allowedValues().contains(allowedValue)) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" is missing allowed value \"" + allowedValue.toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + "\" is missing allowed value \"" + allowedValue.toString() + + "\""); hasError = true; } } if (ifaceActionParamType.minValue() == "any") { if (paramType.minValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" is missing a minimum value"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + "\" is missing a minimum value"); hasError = true; } } else if (!ifaceActionParamType.minValue().isNull()) { if (paramType.minValue() != ifaceActionParamType.minValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" has not matching minimum value: \"" + paramType.minValue().toString() + "\" != \"" + ifaceActionParamType.minValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + "\" has not matching minimum value: \"" + + paramType.minValue().toString() + "\" != \"" + ifaceActionParamType.minValue().toString() + "\""); hasError = true; } } if (ifaceActionParamType.maxValue() == "any") { if (paramType.maxValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" is missing a maximum value"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + "\" is missing a maximum value"); hasError = true; } } else if (!ifaceActionParamType.maxValue().isNull()) { if (paramType.maxValue() != ifaceActionParamType.maxValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" has not matching maximum value: \"" + paramType.maxValue().toString() + "\" != \"" + ifaceActionParamType.maxValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + "\" has not matching maximum value: \"" + + paramType.maxValue().toString() + "\" != \"" + ifaceActionParamType.maxValue().toString() + "\""); hasError = true; } } if (ifaceActionParamType.defaultValue() == "any") { if (paramType.defaultValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" is missing a default value"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + "\" is missing a default value"); hasError = true; } } else if (!ifaceActionParamType.defaultValue().isNull()) { if (paramType.defaultValue() != ifaceActionParamType.defaultValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" is has incompatible default value: \"" + paramType.defaultValue().toString() + "\" != \"" + ifaceActionParamType.defaultValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + "\" is has incompatible default value: \"" + + paramType.defaultValue().toString() + "\" != \"" + ifaceActionParamType.defaultValue().toString() + "\""); hasError = true; } } @@ -887,7 +987,9 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) // name is set or not. if (ifaceActionType.paramTypes().findByName(paramType.name()).name().isEmpty()) { if (paramType.defaultValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + actionType.name() + "\" param \"" + paramType.name() + "\" is missing a default value as the interface requires this action to be executable without params."); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but action \"" + + actionType.name() + "\" param \"" + paramType.name() + + "\" is missing a default value as the interface requires this action to be executable without params."); hasError = true; } } @@ -897,7 +999,8 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) foreach (const InterfaceEventType &ifaceEventType, iface.eventTypes()) { if (!eventTypes.contains(ifaceEventType.name())) { if (!ifaceEventType.optional()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but doesn't implement event \"" + ifaceEventType.name() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + + "\" but doesn't implement event \"" + ifaceEventType.name() + "\""); hasError = true; } continue; @@ -908,49 +1011,63 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) foreach (const ParamType &ifaceEventParamType, ifaceEventType.paramTypes()) { ParamType paramType = eventType.paramTypes().findByName(ifaceEventParamType.name()); if (!paramType.isValid()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" doesn't implement event param \"" + ifaceEventParamType.name() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" doesn't implement event param \"" + ifaceEventParamType.name() + "\""); hasError = true; } else { if (paramType.type() != ifaceEventParamType.type()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" param \"" + paramType.name() + "\" is of wrong type: \"" + QVariant::typeToName(paramType.type()) + "\" expected: \"" + QVariant::typeToName(ifaceEventParamType.type()) + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" param \"" + paramType.name() + "\" is of wrong type: \"" + QVariant::typeToName(paramType.type()) + + "\" expected: \"" + QVariant::typeToName(ifaceEventParamType.type()) + "\""); hasError = true; } foreach (const QVariant &allowedValue, ifaceEventParamType.allowedValues()) { if (!paramType.allowedValues().contains(allowedValue)) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" param \"" + paramType.name() + "\" is missing allowed value \"" + allowedValue.toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" param \"" + paramType.name() + "\" is missing allowed value \"" + allowedValue.toString() + + "\""); hasError = true; } } if (ifaceEventParamType.minValue() == "any") { if (paramType.minValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" param \"" + paramType.name() + "\" is missing a minimum value"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" param \"" + paramType.name() + "\" is missing a minimum value"); hasError = true; } } else if (!ifaceEventParamType.minValue().isNull()) { if (paramType.minValue() != ifaceEventParamType.minValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" param \"" + paramType.name() + "\" has not matching minimum value: \"" + paramType.minValue().toString() + "\" != \"" + ifaceEventParamType.minValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" param \"" + paramType.name() + "\" has not matching minimum value: \"" + + paramType.minValue().toString() + "\" != \"" + ifaceEventParamType.minValue().toString() + "\""); hasError = true; } } if (ifaceEventParamType.maxValue() == "any") { if (paramType.maxValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" param \"" + paramType.name() + "\" is missing a maximum value"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" param \"" + paramType.name() + "\" is missing a maximum value"); hasError = true; } } else if (!ifaceEventParamType.maxValue().isNull()) { if (paramType.maxValue() != ifaceEventParamType.maxValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" param \"" + paramType.name() + "\" has not matching maximum value: \"" + paramType.maxValue().toString() + "\" != \"" + ifaceEventParamType.maxValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" param \"" + paramType.name() + "\" has not matching maximum value: \"" + + paramType.maxValue().toString() + "\" != \"" + ifaceEventParamType.maxValue().toString() + "\""); hasError = true; } } if (ifaceEventParamType.defaultValue().toString() == "any") { if (paramType.defaultValue().isNull()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" param \"" + paramType.name() + "\" is missing a default value"); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" param \"" + paramType.name() + "\" is missing a default value"); hasError = true; } } else if (!ifaceEventParamType.defaultValue().isNull()) { if (paramType.defaultValue() != ifaceEventParamType.defaultValue()) { - m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + eventType.name() + "\" param \"" + paramType.name() + "\" is has incompatible default value: \"" + paramType.defaultValue().toString() + "\" != \"" + ifaceEventParamType.defaultValue().toString() + "\""); + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but event \"" + + eventType.name() + "\" param \"" + paramType.name() + "\" is has incompatible default value: \"" + + paramType.defaultValue().toString() + "\" != \"" + ifaceEventParamType.defaultValue().toString() + "\""); hasError = true; } } @@ -1019,7 +1136,7 @@ QPair PluginMetadata::loadAndVerifyUnit(const QString &unitSt return QPair(false, Types::UnitNone); } - return QPair(true, (Types::Unit)enumValue); + return QPair(true, (Types::Unit) enumValue); } QPair PluginMetadata::verifyFields(const QStringList &possibleFields, const QStringList &mandatoryFields, const QJsonObject &value) @@ -1071,7 +1188,7 @@ QPair PluginMetadata::parseParamTypes(const QJsonArray &array) } // Check type -#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QMetaType::Type t = static_cast(QMetaType::fromName(pt.value("type").toString().toUtf8()).id()); #else QMetaType::Type t = static_cast(QVariant::nameToType(pt.value("type").toString().toLatin1().data())); @@ -1098,7 +1215,6 @@ QPair PluginMetadata::parseParamTypes(const QJsonArray &array) ParamType paramType(paramTypeId, paramName, t, defaultValue); paramType.setDisplayName(pt.value("displayName").toString()); - // Set allowed values QVariantList allowedValues; foreach (const QJsonValue &allowedTypesJson, pt.value("allowedValues").toArray()) { @@ -1144,6 +1260,23 @@ QPair PluginMetadata::parseParamTypes(const QJsonArray &array) // explicitly and convert() would initialize it to the variant's default value maxValue.convert(t); } + + if (pt.contains("stepSize")) { + double stepSize = pt.value("stepSize").toDouble(); + if (stepSize != 0) { + if (!s_validTypesForStepSize.contains(paramType.type())) { + m_validationErrors.append("Param type \"" + paramName + "\" has configured a step size but the data type " + pt.value("type").toString() + + " does not support that. Only numeric state types can have a stepSize."); + hasErrors = true; + } else if (stepSize < 0) { + m_validationErrors.append("Param type \"" + paramName + "\" has configured a negative stepSize which is not allowed."); + hasErrors = true; + } else { + paramType.setStepSize(stepSize); + } + } + } + paramType.setLimits(minValue, maxValue); paramType.setIndex(index++); paramTypes.append(paramType); @@ -1174,7 +1307,7 @@ QPair PluginMetadata::loadAndVerifyInputType(const QStri return QPair(false, Types::InputTypeNone); } - return QPair(true, (Types::InputType)enumValue); + return QPair(true, (Types::InputType) enumValue); } bool PluginMetadata::verifyDuplicateUuid(const QUuid &uuid) diff --git a/libnymea/integrations/thing.cpp b/libnymea/integrations/thing.cpp index 7963fddc..9962f192 100644 --- a/libnymea/integrations/thing.cpp +++ b/libnymea/integrations/thing.cpp @@ -125,33 +125,29 @@ */ #include "thing.h" -#include "types/event.h" #include "loggingcategories.h" #include "statevaluefilters/statevaluefilteradaptive.h" #include "thingutils.h" +#include "types/event.h" -#include #include +#include /*! Construct a Thing with the given \a pluginId, \a id, \a thingClassId and \a parent. */ -Thing::Thing(const PluginId &pluginId, const ThingClass &thingClass, const ThingId &id, QObject *parent): - QObject(parent), - m_thingClass(thingClass), - m_pluginId(pluginId), - m_id(id) -{ - -} +Thing::Thing(const PluginId &pluginId, const ThingClass &thingClass, const ThingId &id, QObject *parent) + : QObject(parent) + , m_thingClass(thingClass) + , m_pluginId(pluginId) + , m_id(id) +{} /*! Construct a Thing with the given \a pluginId, \a thingClassId and \a parent. A new ThingId will be created for this Thing. */ -Thing::Thing(const PluginId &pluginId, const ThingClass &thingClass, QObject *parent): - QObject(parent), - m_thingClass(thingClass), - m_pluginId(pluginId), - m_id(ThingId::createThingId()) -{ - -} +Thing::Thing(const PluginId &pluginId, const ThingClass &thingClass, QObject *parent) + : QObject(parent) + , m_thingClass(thingClass) + , m_pluginId(pluginId) + , m_id(ThingId::createThingId()) +{} Thing::~Thing() { @@ -385,23 +381,99 @@ void Thing::setStateValue(const StateTypeId &stateTypeId, const QVariant &value) if (m_states.at(i).stateTypeId() == stateTypeId) { QVariant newValue = value; if (!newValue.convert(stateType.type())) { - qCWarning(dcThing()).nospace() << this << ": Invalid value " << value << " for state " << stateType.name() << ". Type mismatch. Expected type: " << QVariant::typeToName(stateType.type()) << " (Discarding change)"; + qCWarning(dcThing()).nospace() + << this + << ": Invalid value " + << value + << " for state " + << stateType.name() + << ". Type mismatch. Expected type: " + << QVariant::typeToName(stateType.type()) + << " (Discarding change)"; return; } State state = m_states.at(i); if (state.minValue().isValid() && ThingUtils::variantLessThan(value, state.minValue())) { - qCWarning(dcThing()).nospace() << this << ": Invalid value " << value << " for state " << stateType.name() << ". Out of range: " << state.minValue() << " - " << state.maxValue() << " (Correcting to closest value within range)"; + qCWarning(dcThing()).nospace() + << this + << ": Invalid value " + << value + << " for state " + << stateType.name() + << ". Out of range: " + << state.minValue() + << " - " + << state.maxValue() + << " (Correcting to closest value within range)"; newValue = state.minValue(); } if (state.maxValue().isValid() && ThingUtils::variantGreaterThan(value, state.maxValue())) { - qCWarning(dcThing()).nospace() << this << ": Invalid value " << value << " for state " << stateType.name() << ". Out of range: " << state.minValue() << " - " << state.maxValue() << " (Correcting to closest value within range)"; + qCWarning(dcThing()).nospace() + << this + << ": Invalid value " + << value + << " for state " + << stateType.name() + << ". Out of range: " + << state.minValue() + << " - " + << state.maxValue() + << " (Correcting to closest value within range)"; newValue = state.maxValue(); } if (!stateType.possibleValues().isEmpty() && !stateType.possibleValues().contains(value)) { - qCWarning(dcThing()).nospace() << this << ": Invalid value " << value << " for state " << stateType.name() << ". Not an accepted value. Possible values: " << stateType.possibleValues() << " (Discarding change)"; + qCWarning(dcThing()).nospace() + << this + << ": Invalid value " + << value + << " for state " + << stateType.name() + << ". Not an accepted value. Possible values: " + << stateType.possibleValues() + << " (Discarding change)"; return; } + QVariant clampedValue = ThingUtils::ensureValueClamping(newValue, stateType.type(), state.minValue(), state.maxValue(), stateType.stepSize()); + if (stateType.stepSize() != 0) { + const double stepEpsilon = qMax(qAbs(stateType.stepSize()) * 1e-9, 1e-12); + bool stepAdjusted = false; + switch (stateType.type()) { + case QMetaType::Double: + stepAdjusted = qAbs(newValue.toDouble() - clampedValue.toDouble()) > stepEpsilon; + break; + case QMetaType::Float: + stepAdjusted = qAbs(newValue.toFloat() - clampedValue.toFloat()) > stepEpsilon; + break; + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::LongLong: + case QMetaType::ULongLong: + case QMetaType::Short: + case QMetaType::ULong: + case QMetaType::UShort: + stepAdjusted = (newValue != clampedValue); + break; + default: + break; + } + + if (stepAdjusted) { + newValue = clampedValue; + qCWarning(dcThing()).nospace() + << this + << ": Invalid value " + << value + << " for state " + << stateType.name() + << ". Step size: " + << stateType.stepSize() + << " (Correcting to closest value within step size)"; + } + } else { + newValue = clampedValue; + } + StateValueFilter *filter = m_stateValueFilters.value(stateTypeId); if (filter) { filter->addValue(newValue); @@ -411,7 +483,14 @@ void Thing::setStateValue(const StateTypeId &stateTypeId, const QVariant &value) QVariant oldValue = m_states.at(i).value(); if (oldValue == newValue) { - qCDebug(dcThing()).nospace() << this << ": Discarding state change for " << stateType.name() << " as the value did not actually change. Old value:" << oldValue << "New value:" << newValue; + qCDebug(dcThing()).nospace() + << this + << ": Discarding state change for " + << stateType.name() + << " as the value did not actually change. Old value:" + << oldValue + << "New value:" + << newValue; return; } @@ -457,7 +536,14 @@ void Thing::setStateMinValue(const StateTypeId &stateTypeId, const QVariant &min // Sanity check for max >= min if (ThingUtils::variantLessThan(m_states.at(i).maxValue(), newMin)) { - qCWarning(dcThing()).nospace() << this << ": Adjusting state maximum value for " << stateType.name() << " from " << m_states.at(i).maxValue() << " to new minimum value of " << newMin; + qCWarning(dcThing()).nospace() + << this + << ": Adjusting state maximum value for " + << stateType.name() + << " from " + << m_states.at(i).maxValue() + << " to new minimum value of " + << newMin; m_states[i].setMaxValue(newMin); } if (ThingUtils::variantLessThan(m_states.at(i).value(), newMin)) { @@ -501,12 +587,26 @@ void Thing::setStateMaxValue(const StateTypeId &stateTypeId, const QVariant &max if (newMax.isValid()) { // Sanity check for min <= max if (ThingUtils::variantGreaterThan(m_states.at(i).minValue(), newMax)) { - qCWarning(dcThing()).nospace() << this << ": Adjusting minimum state value for " << stateType.name() << " from " << m_states.at(i).minValue() << " to new maximum value of " << newMax; + qCWarning(dcThing()).nospace() + << this + << ": Adjusting minimum state value for " + << stateType.name() + << " from " + << m_states.at(i).minValue() + << " to new maximum value of " + << newMax; m_states[i].setMinValue(newMax); } if (ThingUtils::variantGreaterThan(m_states.at(i).value(), newMax)) { - qCInfo(dcThing()).nospace() << this << ": Adjusting state value for " << stateType.name() << " from " << m_states.at(i).value() << " to new maximum value of " << newMax; + qCInfo(dcThing()).nospace() + << this + << ": Adjusting state value for " + << stateType.name() + << " from " + << m_states.at(i).value() + << " to new maximum value of " + << newMax; m_states[i].setValue(maxValue); } } @@ -548,16 +648,37 @@ void Thing::setStateMinMaxValues(const StateTypeId &stateTypeId, const QVariant if (newMax.isValid() || newMax.isValid()) { // Sanity check for min <= max if (ThingUtils::variantGreaterThan(newMin, newMax)) { - qCWarning(dcThing()).nospace() << this << ": Adjusting maximum state value for " << stateType.name() << " from " << m_states.at(i).maxValue() << " to new minimum value of " << newMax; + qCWarning(dcThing()).nospace() + << this + << ": Adjusting maximum state value for " + << stateType.name() + << " from " + << m_states.at(i).maxValue() + << " to new minimum value of " + << newMax; m_states[i].setMaxValue(newMin); } if (ThingUtils::variantLessThan(m_states.at(i).value(), m_states.at(i).minValue())) { - qCInfo(dcThing()).nospace() << this << ": Adjusting state value for " << stateType.name() << " from " << m_states.at(i).value() << " to new minimum value of " << m_states.at(i).minValue(); + qCInfo(dcThing()).nospace() + << this + << ": Adjusting state value for " + << stateType.name() + << " from " + << m_states.at(i).value() + << " to new minimum value of " + << m_states.at(i).minValue(); m_states[i].setValue(m_states.at(i).minValue()); } if (ThingUtils::variantGreaterThan(m_states.at(i).value(), m_states.at(i).maxValue())) { - qCInfo(dcThing()).nospace() << this << ": Adjusting state value for " << stateType.name() << " from " << m_states.at(i).value() << " to new maximum value of " << m_states.at(i).maxValue(); + qCInfo(dcThing()).nospace() + << this + << ": Adjusting state value for " + << stateType.name() + << " from " + << m_states.at(i).value() + << " to new maximum value of " + << m_states.at(i).maxValue(); m_states[i].setValue(m_states.at(i).maxValue()); } } @@ -568,7 +689,6 @@ void Thing::setStateMinMaxValues(const StateTypeId &stateTypeId, const QVariant } Q_ASSERT_X(false, m_name.toUtf8(), QString("Failed setting maximum state value %1 to %2").arg(stateType.name()).arg(maxValue.toString()).toUtf8()); qCWarning(dcThing()).nospace() << this << ": Failed setting maximum state value " << stateType.name() << " to " << maxValue; - } void Thing::setStateMinMaxValues(const QString &stateName, const QVariant &minValue, const QVariant &maxValue) @@ -594,10 +714,24 @@ void Thing::setStatePossibleValues(const StateTypeId &stateTypeId, const QVarian if (!values.contains(m_states.value(i).value())) { if (values.contains(stateType.defaultValue())) { - qCInfo(dcThing()).nospace() << this << ": Adjusting state value for " << stateType.name() << " from " << m_states.at(i).value() << " to default value of " << stateType.defaultValue(); + qCInfo(dcThing()).nospace() + << this + << ": Adjusting state value for " + << stateType.name() + << " from " + << m_states.at(i).value() + << " to default value of " + << stateType.defaultValue(); m_states[i].setValue(stateType.defaultValue()); } else if (!values.isEmpty()) { - qCInfo(dcThing()).nospace() << this << ": Adjusting state value for " << stateType.name() << " from " << m_states.at(i).value() << " to new value of " << values.first(); + qCInfo(dcThing()).nospace() + << this + << ": Adjusting state value for " + << stateType.name() + << " from " + << m_states.at(i).value() + << " to new value of " + << values.first(); m_states[i].setValue(values.first()); } } @@ -606,8 +740,9 @@ void Thing::setStatePossibleValues(const StateTypeId &stateTypeId, const QVarian } } qCWarning(dcThing()).nospace() << this << ": Failed setting maximum state value " << stateType.name() << " to " << values; - Q_ASSERT_X(false, m_name.toUtf8(), QString("Failed setting possible state values for %1 to %2").arg(stateType.name()).arg(QString(QJsonDocument::fromVariant(values).toJson())).toUtf8()); - + Q_ASSERT_X(false, + m_name.toUtf8(), + QString("Failed setting possible state values for %1 to %2").arg(stateType.name()).arg(QString(QJsonDocument::fromVariant(values).toJson())).toUtf8()); } /*! Returns the \l{State} with the given \a stateTypeId of this thing. */ @@ -740,9 +875,9 @@ void Thing::setStateValueFilter(const StateTypeId &stateTypeId, Types::StateValu } } -Things::Things(const QList &other) +Things::Things(const QList &other) { - foreach (Thing* thing, other) { + foreach (Thing *thing, other) { this->append(thing); } } @@ -792,7 +927,7 @@ QDebug operator<<(QDebug debug, Thing *thing) Things Things::filterByParam(const ParamTypeId ¶mTypeId, const QVariant &value) { Things ret; - foreach (Thing* thing, *this) { + foreach (Thing *thing, *this) { if (!thing->thingClass().paramTypes().findById(paramTypeId).isValid()) { continue; } @@ -807,7 +942,7 @@ Things Things::filterByParam(const ParamTypeId ¶mTypeId, const QVariant &val Things Things::filterByThingClassId(const ThingClassId &thingClassId) { Things ret; - foreach (Thing* thing, *this) { + foreach (Thing *thing, *this) { if (thing->thingClassId() == thingClassId) { ret << thing; } @@ -844,5 +979,5 @@ QVariant Things::get(int index) const void Things::put(const QVariant &variant) { - append(variant.value()); + append(variant.value()); } diff --git a/libnymea/integrations/thingutils.cpp b/libnymea/integrations/thingutils.cpp index cdf2cca1..70b1a201 100644 --- a/libnymea/integrations/thingutils.cpp +++ b/libnymea/integrations/thingutils.cpp @@ -29,6 +29,29 @@ #include #include #include +#include + +namespace { + +bool isStepSizeType(QMetaType::Type type) +{ + switch (type) { + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::LongLong: + case QMetaType::ULongLong: + case QMetaType::Double: + case QMetaType::Float: + case QMetaType::Short: + case QMetaType::ULong: + case QMetaType::UShort: + return true; + default: + return false; + } +} + +} ThingUtils::ThingUtils() { @@ -134,6 +157,39 @@ Thing::ThingError ThingUtils::verifyParam(const ParamType ¶mType, const Para } } + if (paramType.stepSize() != 0) { + QVariant paramValue = param.value(); + paramValue.convert(static_cast(paramType.type())); + QVariant clampedValue = ThingUtils::ensureValueClamping(paramValue, paramType.type(), paramType.minValue(), paramType.maxValue(), paramType.stepSize()); + const double stepEpsilon = qMax(qAbs(paramType.stepSize()) * 1e-9, 1e-12); + bool stepAdjusted = false; + switch (paramType.type()) { + case QMetaType::Double: + stepAdjusted = qAbs(paramValue.toDouble() - clampedValue.toDouble()) > stepEpsilon; + break; + case QMetaType::Float: + stepAdjusted = qAbs(paramValue.toFloat() - clampedValue.toFloat()) > stepEpsilon; + break; + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::LongLong: + case QMetaType::ULongLong: + case QMetaType::Short: + case QMetaType::ULong: + case QMetaType::UShort: + stepAdjusted = (paramValue != clampedValue); + break; + default: + break; + } + + if (stepAdjusted) { + qCWarning(dcThing()) << "Value not matching step size for param" << param.paramTypeId().toString() + << " Got:" << param.value() << " Step size:" << paramType.stepSize(); + return Thing::ThingErrorInvalidParameter; + } + } + if (!paramType.allowedValues().isEmpty() && !paramType.allowedValues().contains(param.value())) { QStringList allowedValues; foreach (const QVariant &value, paramType.allowedValues()) { @@ -400,6 +456,73 @@ QStringList ThingUtils::generateInterfaceParentList(const QString &interface) return ret; } +QVariant ThingUtils::ensureValueClamping(const QVariant value, QMetaType::Type type, const QVariant &minValue, const QVariant &maxValue, double stepSize) +{ + QVariant adjustedValue = value; + if (!adjustedValue.canConvert(static_cast(type)) || !adjustedValue.convert(static_cast(type))) { + return value; + } + + if (stepSize == 0 || !isStepSizeType(type)) { + if (minValue.isValid() && ThingUtils::variantLessThan(adjustedValue, minValue)) { + return minValue; + } + if (maxValue.isValid() && ThingUtils::variantGreaterThan(adjustedValue, maxValue)) { + return maxValue; + } + return adjustedValue; + } + + const double step = qAbs(stepSize); + const bool hasMinValue = minValue.isValid(); + const bool hasMaxValue = maxValue.isValid(); + const double baseValue = hasMinValue ? minValue.toDouble() : 0.0; + const double currentValue = adjustedValue.toDouble(); + const qint64 roundedSteps = qRound64((currentValue - baseValue) / step); + double steppedValue = baseValue + roundedSteps * step; + + if (hasMinValue) { + const double min = minValue.toDouble(); + if (steppedValue < min) { + steppedValue = min; + } + } + + if (hasMaxValue) { + const double max = maxValue.toDouble(); + if (steppedValue > max) { + const qint64 maxSteps = static_cast(qFloor((max - baseValue) / step)); + steppedValue = baseValue + maxSteps * step; + if (hasMinValue && steppedValue < minValue.toDouble()) { + steppedValue = minValue.toDouble(); + } + } + } + + switch (type) { + case QMetaType::Int: + return QVariant(static_cast(qRound64(steppedValue))); + case QMetaType::UInt: + return QVariant(static_cast(qRound64(steppedValue))); + case QMetaType::LongLong: + return QVariant(static_cast(qRound64(steppedValue))); + case QMetaType::ULongLong: + return QVariant(static_cast(qRound64(steppedValue))); + case QMetaType::Double: + return QVariant(steppedValue); + case QMetaType::Float: + return QVariant(static_cast(steppedValue)); + case QMetaType::Short: + return QVariant::fromValue(static_cast(qRound64(steppedValue))); + case QMetaType::ULong: + return QVariant::fromValue(static_cast(qRound64(steppedValue))); + case QMetaType::UShort: + return QVariant::fromValue(static_cast(qRound64(steppedValue))); + default: + return adjustedValue; + } +} + bool ThingUtils::variantLessThan(const QVariant &leftHandSide, const QVariant &rightHandSide) { // Note: https://www.mail-archive.com/development@qt-project.org/msg39450.html diff --git a/libnymea/integrations/thingutils.h b/libnymea/integrations/thingutils.h index b317bfd6..39e9e313 100644 --- a/libnymea/integrations/thingutils.h +++ b/libnymea/integrations/thingutils.h @@ -26,7 +26,6 @@ #define THINGUTILS_H #include "thing.h" -#include "pluginmetadata.h" #include "types/paramtype.h" #include "types/interface.h" @@ -45,6 +44,8 @@ public: static Interface mergeInterfaces(const Interface &iface1, const Interface &iface2); static QStringList generateInterfaceParentList(const QString &interface); + static QVariant ensureValueClamping(const QVariant value, QMetaType::Type type, const QVariant &minValue, const QVariant &maxValue, double stepSize); + static bool variantLessThan(const QVariant &leftHandSide, const QVariant &rightHandSide); static bool variantGreaterThan(const QVariant &leftHandSide, const QVariant &rightHandSide); }; diff --git a/libnymea/interfaces/evcharger.json b/libnymea/interfaces/evcharger.json index 680f166b..3157a821 100644 --- a/libnymea/interfaces/evcharger.json +++ b/libnymea/interfaces/evcharger.json @@ -1,10 +1,10 @@ { - "description": "An EV-charger. Extends the power interface for charging/not charging an electric vehicle. Supports regulation of the max. charging current in addition to be powered on or off.", + "description": "An electric vehicle charger. Extends the power interface to report plug/charge state and session energy, and to optionally control charging current and phase count in addition to on/off power control.", "extends": ["power"], "states": [ { "name": "maxChargingCurrent", - "type": "uint", + "type": "double", "writable": true, "unit": "Ampere", "minValue": "any", diff --git a/libnymea/types/paramtype.cpp b/libnymea/types/paramtype.cpp index ae849f4b..b072d7ee 100644 --- a/libnymea/types/paramtype.cpp +++ b/libnymea/types/paramtype.cpp @@ -38,22 +38,15 @@ \sa isValid() */ - #include "paramtype.h" /*! Constructs a ParamType object with the given \a id, \a name, \a type and \a defaultValue. */ -ParamType::ParamType(const ParamTypeId &id, const QString &name, const QMetaType::Type type, const QVariant &defaultValue): - m_id(id), - m_name(name), - m_index(0), - m_type(type), - m_defaultValue(defaultValue), - m_inputType(Types::InputTypeNone), - m_unit(Types::UnitNone), - m_readOnly(false) -{ - -} +ParamType::ParamType(const ParamTypeId &id, const QString &name, const QMetaType::Type type, const QVariant &defaultValue) + : m_id(id) + , m_name(name) + , m_type(type) + , m_defaultValue(defaultValue) +{} /*! Returns the \l{ParamTypeId} of this ParamType. */ ParamTypeId ParamType::id() const @@ -145,6 +138,18 @@ void ParamType::setMaxValue(const QVariant &maxValue) m_maxValue = maxValue; } +/*! Returns the step size for the value of this ParamType. If there are no steps to consider, it returns 0. */ +double ParamType::stepSize() const +{ + return m_stepSize; +} + +/*! Sets the step size for the value of this ParamType to the given \a stepSize. If there are no steps to consider, it can be set to 0. */ +void ParamType::setStepSize(double stepSize) +{ + m_stepSize = stepSize; +} + /*! Returns the input type of this ParamType. */ Types::InputType ParamType::inputType() const { @@ -215,8 +220,18 @@ bool ParamType::isValid() const /*! Returns a list of all valid JSON properties a ParamType JSON definition can have. */ QStringList ParamType::typeProperties() { - return QStringList() << "id" << "name" << "displayName" << "type" << "defaultValue" << "inputType" - << "unit" << "minValue" << "maxValue" << "allowedValues" << "readOnly"; + return QStringList() << "id" + << "name" + << "displayName" + << "type" + << "defaultValue" + << "inputType" + << "unit" + << "minValue" + << "maxValue" + << "stepSize" + << "allowedValues" + << "readOnly"; } /*! Returns a list of mandatory JSON properties a ParamType JSON definition must have. */ @@ -229,22 +244,16 @@ QStringList ParamType::mandatoryTypeProperties() QDebug operator<<(QDebug dbg, const ParamType ¶mType) { QString typeName; -#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) typeName = QString(QMetaType(paramType.type()).name()); #else typeName = QVariant::typeToName(paramType.type()); #endif QDebugStateSaver saver(dbg); - dbg.nospace() << "ParamType(Id" << paramType.id().toString() - << " Name: " << paramType.name() - << ", Type:" << typeName - << ", Default:" << paramType.defaultValue() - << ", Min:" << paramType.minValue() - << ", Max:" << paramType.maxValue() - << ", Allowed values:" << paramType.allowedValues() - << ", ReadOnly:" << paramType.readOnly() - << ")"; + dbg.nospace() << "ParamType(Id" << paramType.id().toString() << " Name: " << paramType.name() << ", Type:" << typeName << ", Default:" << paramType.defaultValue() + << ", Min:" << paramType.minValue() << ", Max:" << paramType.maxValue() << ", Allowed values:" << paramType.allowedValues() + << ", ReadOnly:" << paramType.readOnly() << ")"; return dbg; } @@ -254,16 +263,16 @@ QDebug operator<<(QDebug dbg, const QList ¶mTypes) { QDebugStateSaver saver(dbg); dbg.nospace() << "ParamTypeList (count:" << paramTypes.count() << ")" << '\n'; - for (int i = 0; i < paramTypes.count(); i++ ) { + for (int i = 0; i < paramTypes.count(); i++) { dbg.nospace() << " " << i << ": " << paramTypes.at(i) << '\n'; } return dbg; } -ParamTypes::ParamTypes(const QList &other): QList(other) -{ -} +ParamTypes::ParamTypes(const QList &other) + : QList(other) +{} bool ParamTypes::contains(const ParamTypeId ¶mTypeId) { diff --git a/libnymea/types/paramtype.h b/libnymea/types/paramtype.h index a9f975d4..2b3101f8 100644 --- a/libnymea/types/paramtype.h +++ b/libnymea/types/paramtype.h @@ -25,8 +25,8 @@ #ifndef PARAMTYPE_H #define PARAMTYPE_H -#include #include +#include #include "libnymea.h" #include "typeutils.h" @@ -42,6 +42,7 @@ class LIBNYMEA_EXPORT ParamType Q_PROPERTY(QVariant defaultValue READ defaultValue WRITE setDefaultValue USER true) Q_PROPERTY(QVariant minValue READ minValue WRITE setMinValue USER true) Q_PROPERTY(QVariant maxValue READ maxValue WRITE setMaxValue USER true) + Q_PROPERTY(double stepSize READ stepSize WRITE setStepSize USER true) Q_PROPERTY(QVariantList allowedValues READ allowedValues WRITE setAllowedValues USER true) Q_PROPERTY(Types::InputType inputType READ inputType WRITE setInputType USER true) Q_PROPERTY(Types::Unit unit READ unit WRITE setUnit USER true) @@ -74,6 +75,9 @@ public: QVariant maxValue() const; void setMaxValue(const QVariant &maxValue); + double stepSize() const; + void setStepSize(double stepSize); + Types::InputType inputType() const; void setInputType(const Types::InputType &inputType); @@ -98,18 +102,19 @@ private: ParamTypeId m_id; QString m_name; QString m_displayName; - int m_index; + int m_index = 0; QMetaType::Type m_type; QVariant m_defaultValue; QVariant m_minValue; QVariant m_maxValue; - Types::InputType m_inputType; - Types::Unit m_unit; + double m_stepSize = 0; + Types::InputType m_inputType = Types::InputTypeNone; + Types::Unit m_unit = Types::UnitNone; QVariantList m_allowedValues; - bool m_readOnly; + bool m_readOnly = false; }; -class ParamTypes: public QList +class ParamTypes : public QList { Q_GADGET Q_PROPERTY(int count READ count) @@ -123,7 +128,6 @@ public: ParamType findByName(const QString &name) const; ParamType findById(const ParamTypeId &id) const; ParamType &operator[](const QString &name); - }; Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(ParamTypes) diff --git a/libnymea/types/statetype.cpp b/libnymea/types/statetype.cpp index 79f94570..d946f3ff 100644 --- a/libnymea/types/statetype.cpp +++ b/libnymea/types/statetype.cpp @@ -34,19 +34,14 @@ #include "statetype.h" -StateType::StateType() -{ - -} +StateType::StateType() {} /*! Constructs a StateType with the given \a id. * When creating a \l{DevicePlugin} generate a new uuid for each StateType you define and * hardcode it into the plugin json file. */ -StateType::StateType(const StateTypeId &id): - m_id(id) -{ - -} +StateType::StateType(const StateTypeId &id) + : m_id(id) +{} /*! Returns the id of the StateType. */ StateTypeId StateType::id() const @@ -141,6 +136,18 @@ void StateType::setMaxValue(const QVariant &maxValue) m_maxValue = maxValue; } +/*! Returns the step size for the value of this StateType. If there are no steps to consider, the value will be 0. */ +double StateType::stepSize() const +{ + return m_stepSize; +} + +/*! Set the step size for the value of this StateType to \a stepSize. If there are no steps to consider, the value can be set to 0. */ +void StateType::setStepSize(double stepSize) +{ + m_stepSize = stepSize; +} + /*! Returns the list of possible values of this StateType. If the list is empty or invalid the \l{State} value can take every value. */ QVariantList StateType::possibleValues() const { diff --git a/libnymea/types/statetype.h b/libnymea/types/statetype.h index 6591a7d8..116ba0f4 100644 --- a/libnymea/types/statetype.h +++ b/libnymea/types/statetype.h @@ -43,6 +43,7 @@ class LIBNYMEA_EXPORT StateType Q_PROPERTY(Types::IOType ioType READ ioType WRITE setIOType USER true) Q_PROPERTY(QVariant minValue READ minValue WRITE setMinValue USER true) Q_PROPERTY(QVariant maxValue READ maxValue WRITE setMaxValue USER true) + Q_PROPERTY(double stepSize READ stepSize WRITE setStepSize USER true) Q_PROPERTY(QVariantList possibleValues READ possibleValues WRITE setPossibleValues USER true) Q_PROPERTY(QStringList possibleValuesDisplayNames READ possibleValuesDisplayNames WRITE setPossibleValuesDisplayNames USER true) @@ -73,6 +74,9 @@ public: QVariant maxValue() const; void setMaxValue(const QVariant &maxValue); + double stepSize() const; + void setStepSize(double stepSize); + QVariantList possibleValues() const; void setPossibleValues(const QVariantList &possibleValues); @@ -108,6 +112,7 @@ private: QVariant m_defaultValue; QVariant m_minValue; QVariant m_maxValue; + double m_stepSize = 0; QVariantList m_possibleValues; QStringList m_possibleValuesDisplayNames; Types::Unit m_unit = Types::UnitNone; @@ -119,7 +124,7 @@ private: }; Q_DECLARE_METATYPE(StateType) -class StateTypes: public QList +class StateTypes : public QList { Q_GADGET Q_PROPERTY(int count READ count) diff --git a/tests/auto/api.json b/tests/auto/api.json index 595f40de..69644ae9 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -3039,6 +3039,7 @@ "o:maxValue": "Variant", "o:minValue": "Variant", "o:readOnly": "Bool", + "o:stepSize": "Double", "o:unit": "$ref:Unit", "r:id": "Uuid", "type": "$ref:BasicType" @@ -3171,6 +3172,7 @@ "Variant" ], "o:possibleValuesDisplayNames": "StringList", + "o:stepSize": "Double", "o:unit": "$ref:Unit", "r:id": "Uuid", "type": "$ref:BasicType" diff --git a/tests/auto/integrations/testintegrations.cpp b/tests/auto/integrations/testintegrations.cpp index 865e6563..9010eec5 100644 --- a/tests/auto/integrations/testintegrations.cpp +++ b/tests/auto/integrations/testintegrations.cpp @@ -28,11 +28,14 @@ #include "integrations/thingdiscoveryinfo.h" #include "integrations/thingsetupinfo.h" +#include "integrations/thingutils.h" #include "servers/mocktcpserver.h" #include "jsonrpc/integrationshandler.h" #include "../plugins/mock/extern-plugininfo.h" +#include + using namespace nymeaserver; class TestIntegrations : public NymeaTestBase @@ -136,6 +139,7 @@ private slots: void triggerStateChangeSignal(); void params(); + void ensureValueClamping(); void dynamicMinMax(); @@ -2191,6 +2195,26 @@ void TestIntegrations::params() QVERIFY(!event.param(ParamTypeId::createParamTypeId()).value().isValid()); } +void TestIntegrations::ensureValueClamping() +{ + const double tolerance = 1e-6; + + QVariant result = ThingUtils::ensureValueClamping(5.789, QMetaType::Double, 0.0, 10.0, 0.1); + QVERIFY(qAbs(result.toDouble() - 5.8) < tolerance); + + result = ThingUtils::ensureValueClamping(1.3, QMetaType::Double, 1.0, 5.0, 0.5); + QVERIFY(qAbs(result.toDouble() - 1.5) < tolerance); + + result = ThingUtils::ensureValueClamping(11.0, QMetaType::Double, 0.0, 10.0, 3.0); + QVERIFY(qAbs(result.toDouble() - 9.0) < tolerance); + + result = ThingUtils::ensureValueClamping(7, QMetaType::Int, 0, 20, 5); + QCOMPARE(result.toInt(), 5); + + result = ThingUtils::ensureValueClamping(-5, QMetaType::Int, 0, 20, 0); + QCOMPARE(result.toInt(), 0); +} + void TestIntegrations::dynamicMinMax() { enableNotifications({"Integrations"}); @@ -2396,4 +2420,3 @@ void TestIntegrations::testTranslations() #include "testintegrations.moc" QTEST_MAIN(TestIntegrations) -