diff --git a/docs/api.md b/docs/api.md index b0f2705df12cc2f88b221a58f8783a991a5d93d5..72f36f32bec303fef6efe68d20a70117dfb2afe6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -309,6 +309,16 @@ scripting functionality.</p> <dd><p>This module provides the APIs for validating inputs to the database, and returning flags indicating if things were successful or not.</p> </dd> +<dt><a href="#module_Validation API_ check element conformance">Validation API: check element conformance</a></dt> +<dd><p>This module provides utilities for checking if elements meet conformance requirements +and generate warnings for non-conformance.</p> +</dd> +<dt><a href="#module_Validation API_ Evaluate conformance expressions">Validation API: Evaluate conformance expressions</a></dt> +<dd><p>This module provides utilities for evaluating conformance expressions.</p> +</dd> +<dt><a href="#module_Validation API_ Parse conformance data from XML">Validation API: Parse conformance data from XML</a></dt> +<dd><p>This module provides utilities for parsing conformance data from XML into expressions.</p> +</dd> <dt><a href="#module_Validation API_ Validation APIs">Validation API: Validation APIs</a></dt> <dd><p>This module provides the APIs for validating inputs to the database, and returning flags indicating if things were successful or not.</p> @@ -3645,20 +3655,7 @@ This module provides queries for features. * [DB API: feature related queries](#module_DB API_ feature related queries) * [~getFeaturesByDeviceTypeRefs(db, deviceTypeRefs, endpointTypeRef)](#module_DB API_ feature related queries..getFeaturesByDeviceTypeRefs) ⇒ - * [~evaluateConformanceExpression(expression, elementMap)](#module_DB API_ feature related queries..evaluateConformanceExpression) ⇒ - * [~evaluateBooleanExpression(expr)](#module_DB API_ feature related queries..evaluateConformanceExpression..evaluateBooleanExpression) - * [~evaluateWithParentheses(expr)](#module_DB API_ feature related queries..evaluateConformanceExpression..evaluateWithParentheses) - * [~checkMissingTerms(expression, elementMap)](#module_DB API_ feature related queries..checkMissingTerms) ⇒ - * [~filterElementsContainingDesc(elements)](#module_DB API_ feature related queries..filterElementsContainingDesc) ⇒ - * [~filterRelatedDescElements(elements, featureCode)](#module_DB API_ feature related queries..filterRelatedDescElements) ⇒ - * [~generateWarningMessage(featureData, endpointId, elementMap, featureMap, descElements)](#module_DB API_ feature related queries..generateWarningMessage) ⇒ - * [~checkElementConformance(elements, featureMap, featureData, endpointId)](#module_DB API_ feature related queries..checkElementConformance) ⇒ - * [~filterElementsToUpdate(elements, elementMap, featureCode)](#module_DB API_ feature related queries..filterElementsToUpdate) ⇒ - * [~getOutdatedElementWarning(featureData, elements, elementMap)](#module_DB API_ feature related queries..getOutdatedElementWarning) ⇒ - * [~processElements(elementType)](#module_DB API_ feature related queries..getOutdatedElementWarning..processElements) - * [~filterRequiredElements(elements, elementMap, featureMap)](#module_DB API_ feature related queries..filterRequiredElements) ⇒ * [~checkIfConformanceDataExist(db)](#module_DB API_ feature related queries..checkIfConformanceDataExist) ⇒ - * [~getEndpointTypeElements(db, endpointTypeClusterId, deviceTypeClusterId)](#module_DB API_ feature related queries..getEndpointTypeElements) ⇒ <a name="module_DB API_ feature related queries..getFeaturesByDeviceTypeRefs"></a> @@ -3678,182 +3675,6 @@ with associated device type, cluster, and featureMap attribute details | deviceTypeRefs | <code>\*</code> | | endpointTypeRef | <code>\*</code> | -<a name="module_DB API_ feature related queries..evaluateConformanceExpression"></a> - -### DB API: feature related queries~evaluateConformanceExpression(expression, elementMap) ⇒ -Evaluate the value of a boolean conformance expression that includes terms and operators. -A term can be an attribute, command, event, feature, or conformance abbreviation. -Operators include AND (&), OR (|), and NOT (!). -The '[]' indicates optional conformance if the expression inside true. -Expression containing comma means otherwise conformance. See spec for details. -Examples of conformance expression: 'A & (!B | C)', 'A & B, [!C]' - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: 'mandatory', 'optional', 'provisional', or 'notSupported' - -| Param | Type | -| --- | --- | -| expression | <code>\*</code> | -| elementMap | <code>\*</code> | - - -* [~evaluateConformanceExpression(expression, elementMap)](#module_DB API_ feature related queries..evaluateConformanceExpression) ⇒ - * [~evaluateBooleanExpression(expr)](#module_DB API_ feature related queries..evaluateConformanceExpression..evaluateBooleanExpression) - * [~evaluateWithParentheses(expr)](#module_DB API_ feature related queries..evaluateConformanceExpression..evaluateWithParentheses) - -<a name="module_DB API_ feature related queries..evaluateConformanceExpression..evaluateBooleanExpression"></a> - -#### evaluateConformanceExpression~evaluateBooleanExpression(expr) -helper function to evaluate a single boolean expression - -**Kind**: inner method of [<code>evaluateConformanceExpression</code>](#module_DB API_ feature related queries..evaluateConformanceExpression) - -| Param | Type | -| --- | --- | -| expr | <code>\*</code> | - -<a name="module_DB API_ feature related queries..evaluateConformanceExpression..evaluateWithParentheses"></a> - -#### evaluateConformanceExpression~evaluateWithParentheses(expr) -helper function to process parentheses and evaluate inner expressions first - -**Kind**: inner method of [<code>evaluateConformanceExpression</code>](#module_DB API_ feature related queries..evaluateConformanceExpression) - -| Param | Type | -| --- | --- | -| expr | <code>\*</code> | - -<a name="module_DB API_ feature related queries..checkMissingTerms"></a> - -### DB API: feature related queries~checkMissingTerms(expression, elementMap) ⇒ -Check if any terms in the expression are neither a key in the elementMap nor an abbreviation. -If so, it means the conformance depends on terms with unknown values and changes are not allowed. - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: all missing terms in an array - -| Param | Type | -| --- | --- | -| expression | <code>\*</code> | -| elementMap | <code>\*</code> | - -<a name="module_DB API_ feature related queries..filterElementsContainingDesc"></a> - -### DB API: feature related queries~filterElementsContainingDesc(elements) ⇒ -Filter an array of elements by if any element has conformance containing the term 'desc'. - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: elements with conformance containing 'desc' - -| Param | Type | -| --- | --- | -| elements | <code>\*</code> | - -<a name="module_DB API_ feature related queries..filterRelatedDescElements"></a> - -### DB API: feature related queries~filterRelatedDescElements(elements, featureCode) ⇒ -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: elements with conformance containing 'desc' and the feature code - -| Param | Type | -| --- | --- | -| elements | <code>\*</code> | -| featureCode | <code>\*</code> | - -<a name="module_DB API_ feature related queries..generateWarningMessage"></a> - -### DB API: feature related queries~generateWarningMessage(featureData, endpointId, elementMap, featureMap, descElements) ⇒ -Generate a warning message after processing conformance of the updated device type feature. -Set flags to decide whether to show warnings or disable changes in the frontend. - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: warning message array, disableChange flag, and displayWarning flag - -| Param | Type | -| --- | --- | -| featureData | <code>\*</code> | -| endpointId | <code>\*</code> | -| elementMap | <code>\*</code> | -| featureMap | <code>\*</code> | -| descElements | <code>\*</code> | - -<a name="module_DB API_ feature related queries..checkElementConformance"></a> - -### DB API: feature related queries~checkElementConformance(elements, featureMap, featureData, endpointId) ⇒ -Check if elements need to be updated for correct conformance if featureData provided. -Otherwise, check if elements are required or unsupported by their conformance. - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: attributes, commands, and events to update, with warnings if featureData provided; -required and unsupported attributes, commands, and events, with warnings if not. - -| Param | Type | Default | -| --- | --- | --- | -| elements | <code>\*</code> | | -| featureMap | <code>\*</code> | | -| featureData | <code>\*</code> | <code></code> | -| endpointId | <code>\*</code> | <code></code> | - -<a name="module_DB API_ feature related queries..filterElementsToUpdate"></a> - -### DB API: feature related queries~filterElementsToUpdate(elements, elementMap, featureCode) ⇒ -Return attributes, commands, or events to be updated satisfying: -(1) its conformance includes feature code of the updated feature -(2) it has mandatory conformance but it is not enabled, OR, - it is has notSupported conformance but it is enabled - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: elements that should be updated - -| Param | Type | -| --- | --- | -| elements | <code>\*</code> | -| elementMap | <code>\*</code> | -| featureCode | <code>\*</code> | - -<a name="module_DB API_ feature related queries..getOutdatedElementWarning"></a> - -### DB API: feature related queries~getOutdatedElementWarning(featureData, elements, elementMap) ⇒ -Get warnings for element requirements that are outdated after a feature update. - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: array of outdated element warnings - -| Param | Type | -| --- | --- | -| featureData | <code>\*</code> | -| elements | <code>\*</code> | -| elementMap | <code>\*</code> | - -<a name="module_DB API_ feature related queries..getOutdatedElementWarning..processElements"></a> - -#### getOutdatedElementWarning~processElements(elementType) -Build substrings of outdated warnings and add to returned array if: -(1) the element conformance includes the feature code -(2) the element conformance has changed after the feature update - -**Kind**: inner method of [<code>getOutdatedElementWarning</code>](#module_DB API_ feature related queries..getOutdatedElementWarning) - -| Param | Type | -| --- | --- | -| elementType | <code>\*</code> | - -<a name="module_DB API_ feature related queries..filterRequiredElements"></a> - -### DB API: feature related queries~filterRequiredElements(elements, elementMap, featureMap) ⇒ -Filter required and unsupported elements based on their conformance and generate warnings. -An element is required if it conforms to element(s) in elementMap and has 'mandatory' conform. -An element is unsupported if it conforms to element(s) in elementMap and has 'notSupported' conform. - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: required and not supported elements with warnings - -| Param | Type | -| --- | --- | -| elements | <code>\*</code> | -| elementMap | <code>\*</code> | -| featureMap | <code>\*</code> | - <a name="module_DB API_ feature related queries..checkIfConformanceDataExist"></a> ### DB API: feature related queries~checkIfConformanceDataExist(db) ⇒ @@ -3867,21 +3688,6 @@ and DEVICE_TYPE_FEATURE table. | --- | --- | | db | <code>\*</code> | -<a name="module_DB API_ feature related queries..getEndpointTypeElements"></a> - -### DB API: feature related queries~getEndpointTypeElements(db, endpointTypeClusterId, deviceTypeClusterId) ⇒ -Get all attributes, commands and events in an endpoint type cluster. - -**Kind**: inner method of [<code>DB API: feature related queries</code>](#module_DB API_ feature related queries) -**Returns**: elements object containing all attributes, commands and events -in an endpoint type cluster - -| Param | Type | -| --- | --- | -| db | <code>\*</code> | -| endpointTypeClusterId | <code>\*</code> | -| deviceTypeClusterId | <code>\*</code> | - <a name="module_DB API_ package-based queries."></a> ## DB API: package-based queries. @@ -19640,103 +19446,386 @@ Check if float value is within the min/max bounds. | min | <code>\*</code> | | max | <code>\*</code> | -<a name="module_Validation API_ Validation APIs"></a> - -## Validation API: Validation APIs -This module provides the APIs for validating inputs to the database, and returning flags indicating if -things were successful or not. - - -* [Validation API: Validation APIs](#module_Validation API_ Validation APIs) - * _static_ - * [.initAsyncValidation()](#module_Validation API_ Validation APIs.initAsyncValidation) - * _inner_ - * [~zigbeeEnforceCommonClusterSpecInit(session)](#module_Validation API_ Validation APIs..zigbeeEnforceCommonClusterSpecInit) ⇒ - * [~zigbeeEnforceCommonClusterSpecCallback(session)](#module_Validation API_ Validation APIs..zigbeeEnforceCommonClusterSpecCallback) - * [~validateAttribute(db, endpointTypeId, attributeRef, clusterRef, zapSessionId)](#module_Validation API_ Validation APIs..validateAttribute) ⇒ - * [~validateEndpoint(db, endpointId)](#module_Validation API_ Validation APIs..validateEndpoint) ⇒ - * [~validateNoDuplicateEndpoints(db, endpointIdentifier, sessionRef)](#module_Validation API_ Validation APIs..validateNoDuplicateEndpoints) ⇒ - * [~validateSpecificAttribute(endpointAttribute, attribute, db, zapSessionId)](#module_Validation API_ Validation APIs..validateSpecificAttribute) ⇒ - * [~validateSpecificEndpoint(endpoint)](#module_Validation API_ Validation APIs..validateSpecificEndpoint) ⇒ - * [~isValidNumberString(value)](#module_Validation API_ Validation APIs..isValidNumberString) ⇒ - * [~isValidHexString(value)](#module_Validation API_ Validation APIs..isValidHexString) ⇒ - * [~isValidDecimalString(value)](#module_Validation API_ Validation APIs..isValidDecimalString) ⇒ - * [~isValidFloat(value)](#module_Validation API_ Validation APIs..isValidFloat) ⇒ - * [~extractFloatValue(value)](#module_Validation API_ Validation APIs..extractFloatValue) ⇒ - * [~extractIntegerValue(value)](#module_Validation API_ Validation APIs..extractIntegerValue) ⇒ - * [~extractBigIntegerValue(value)](#module_Validation API_ Validation APIs..extractBigIntegerValue) ⇒ - * [~isBigInteger(bits)](#module_Validation API_ Validation APIs..isBigInteger) ⇒ - * [~getBoundsInteger(attribute, typeSize, isSigned)](#module_Validation API_ Validation APIs..getBoundsInteger) ⇒ - * [~getTypeRange(typeSize, isSigned, isMin)](#module_Validation API_ Validation APIs..getTypeRange) ⇒ - * [~unsignedToSignedInteger(value, typeSize)](#module_Validation API_ Validation APIs..unsignedToSignedInteger) ⇒ - * [~getIntegerFromAttribute(attribute, typeSize, isSigned)](#module_Validation API_ Validation APIs..getIntegerFromAttribute) ⇒ - * [~getIntegerAttributeSize(db, zapSessionId, attribType)](#module_Validation API_ Validation APIs..getIntegerAttributeSize) ⇒ <code>\*</code> - * [~checkAttributeBoundsInteger(attribute, endpointAttribute, db, zapSessionId)](#module_Validation API_ Validation APIs..checkAttributeBoundsInteger) ⇒ - * [~checkBoundsInteger(defaultValue, min, max)](#module_Validation API_ Validation APIs..checkBoundsInteger) ⇒ - * [~checkAttributeBoundsFloat(attribute, endpointAttribute)](#module_Validation API_ Validation APIs..checkAttributeBoundsFloat) ⇒ - * [~getBoundsFloat(attribute)](#module_Validation API_ Validation APIs..getBoundsFloat) ⇒ - * [~checkBoundsFloat(defaultValue, min, max)](#module_Validation API_ Validation APIs..checkBoundsFloat) ⇒ +<a name="module_Validation API_ check element conformance"></a> -<a name="module_Validation API_ Validation APIs.initAsyncValidation"></a> +## Validation API: check element conformance +This module provides utilities for checking if elements meet conformance requirements +and generate warnings for non-conformance. -### Validation API: Validation APIs.initAsyncValidation() -Start session specific validation. -**Kind**: static method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) -<a name="module_Validation API_ Validation APIs..zigbeeEnforceCommonClusterSpecInit"></a> +* [Validation API: check element conformance](#module_Validation API_ check element conformance) + * [~filterRelatedDescElements(elements, featureCode)](#module_Validation API_ check element conformance..filterRelatedDescElements) ⇒ + * [~generateWarningMessage(featureData, endpointId, elementMap, featureMap, descElements)](#module_Validation API_ check element conformance..generateWarningMessage) ⇒ + * [~checkElementConformance(elements, featureMap, featureData, endpointId)](#module_Validation API_ check element conformance..checkElementConformance) ⇒ + * [~filterElementsToUpdate(elements, elementMap, featureCode)](#module_Validation API_ check element conformance..filterElementsToUpdate) ⇒ + * [~getOutdatedElementWarning(featureData, elements, elementMap)](#module_Validation API_ check element conformance..getOutdatedElementWarning) ⇒ + * [~processElements(elementType)](#module_Validation API_ check element conformance..getOutdatedElementWarning..processElements) + * [~filterRequiredElements(elements, elementMap, featureMap)](#module_Validation API_ check element conformance..filterRequiredElements) ⇒ + * [~setConformanceWarnings(db, endpointId, endpointTypeId, endpointClusterId, deviceTypeRefs, cluster, sessionId)](#module_Validation API_ check element conformance..setConformanceWarnings) ⇒ -### Validation API: Validation APIs~zigbeeEnforceCommonClusterSpecInit(session) ⇒ -Enforce zigbee specific common cluster initialization. +<a name="module_Validation API_ check element conformance..filterRelatedDescElements"></a> -**Kind**: inner method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) -**Returns**: object +### Validation API: check element conformance~filterRelatedDescElements(elements, featureCode) ⇒ +**Kind**: inner method of [<code>Validation API: check element conformance</code>](#module_Validation API_ check element conformance) +**Returns**: elements with conformance containing 'desc' and the feature code | Param | Type | | --- | --- | -| session | <code>\*</code> | +| elements | <code>\*</code> | +| featureCode | <code>\*</code> | -<a name="module_Validation API_ Validation APIs..zigbeeEnforceCommonClusterSpecCallback"></a> +<a name="module_Validation API_ check element conformance..generateWarningMessage"></a> -### Validation API: Validation APIs~zigbeeEnforceCommonClusterSpecCallback(session) -Enforce zigbee specific common cluster initialization. +### Validation API: check element conformance~generateWarningMessage(featureData, endpointId, elementMap, featureMap, descElements) ⇒ +Generate a warning message after processing conformance of the updated device type feature. +Set flags to decide whether to show warnings or disable changes in the frontend. -**Kind**: inner method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) +**Kind**: inner method of [<code>Validation API: check element conformance</code>](#module_Validation API_ check element conformance) +**Returns**: warning message array, disableChange flag, and displayWarning flag | Param | Type | | --- | --- | -| session | <code>\*</code> | +| featureData | <code>\*</code> | +| endpointId | <code>\*</code> | +| elementMap | <code>\*</code> | +| featureMap | <code>\*</code> | +| descElements | <code>\*</code> | -<a name="module_Validation API_ Validation APIs..validateAttribute"></a> +<a name="module_Validation API_ check element conformance..checkElementConformance"></a> -### Validation API: Validation APIs~validateAttribute(db, endpointTypeId, attributeRef, clusterRef, zapSessionId) ⇒ -Main attribute validation function. -Returns a promise of an object which stores a list of validation issues. -Such issues as "Invalid type" or "Out of Range". +### Validation API: check element conformance~checkElementConformance(elements, featureMap, featureData, endpointId) ⇒ +Check if elements need to be updated for correct conformance if featureData provided. +Otherwise, check if elements are required or unsupported by their conformance. -**Kind**: inner method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) -**Returns**: Promise of the list of issues +**Kind**: inner method of [<code>Validation API: check element conformance</code>](#module_Validation API_ check element conformance) +**Returns**: attributes, commands, and events to update, with warnings if featureData provided; +required and unsupported attributes, commands, and events, with warnings if not. -| Param | Type | Description | +| Param | Type | Default | | --- | --- | --- | -| db | <code>\*</code> | db reference | -| endpointTypeId | <code>\*</code> | endpoint reference | -| attributeRef | <code>\*</code> | attribute reference | -| clusterRef | <code>\*</code> | cluster reference | -| zapSessionId | <code>\*</code> | session reference | +| elements | <code>\*</code> | | +| featureMap | <code>\*</code> | | +| featureData | <code>\*</code> | <code></code> | +| endpointId | <code>\*</code> | <code></code> | -<a name="module_Validation API_ Validation APIs..validateEndpoint"></a> +<a name="module_Validation API_ check element conformance..filterElementsToUpdate"></a> -### Validation API: Validation APIs~validateEndpoint(db, endpointId) ⇒ -Get issues in an endpoint. +### Validation API: check element conformance~filterElementsToUpdate(elements, elementMap, featureCode) ⇒ +Return attributes, commands, or events to be updated satisfying: +(1) its conformance includes feature code of the updated feature +(2) it has mandatory conformance but it is not enabled, OR, + it is has notSupported conformance but it is enabled -**Kind**: inner method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) -**Returns**: object +**Kind**: inner method of [<code>Validation API: check element conformance</code>](#module_Validation API_ check element conformance) +**Returns**: elements that should be updated | Param | Type | | --- | --- | -| db | <code>\*</code> | -| endpointId | <code>\*</code> | - +| elements | <code>\*</code> | +| elementMap | <code>\*</code> | +| featureCode | <code>\*</code> | + +<a name="module_Validation API_ check element conformance..getOutdatedElementWarning"></a> + +### Validation API: check element conformance~getOutdatedElementWarning(featureData, elements, elementMap) ⇒ +Get warnings for element requirements that are outdated after a feature update. + +**Kind**: inner method of [<code>Validation API: check element conformance</code>](#module_Validation API_ check element conformance) +**Returns**: array of outdated element warnings + +| Param | Type | +| --- | --- | +| featureData | <code>\*</code> | +| elements | <code>\*</code> | +| elementMap | <code>\*</code> | + +<a name="module_Validation API_ check element conformance..getOutdatedElementWarning..processElements"></a> + +#### getOutdatedElementWarning~processElements(elementType) +Build substrings of outdated warnings and add to returned array if: +(1) the element conformance includes the feature code +(2) the element conformance has changed after the feature update + +**Kind**: inner method of [<code>getOutdatedElementWarning</code>](#module_Validation API_ check element conformance..getOutdatedElementWarning) + +| Param | Type | +| --- | --- | +| elementType | <code>\*</code> | + +<a name="module_Validation API_ check element conformance..filterRequiredElements"></a> + +### Validation API: check element conformance~filterRequiredElements(elements, elementMap, featureMap) ⇒ +Filter required and unsupported elements based on their conformance and generate warnings. +An element is required if it conforms to element(s) in elementMap and has 'mandatory' conform. +An element is unsupported if it conforms to element(s) in elementMap and has 'notSupported' conform. + +**Kind**: inner method of [<code>Validation API: check element conformance</code>](#module_Validation API_ check element conformance) +**Returns**: required and not supported elements with warnings + +| Param | Type | +| --- | --- | +| elements | <code>\*</code> | +| elementMap | <code>\*</code> | +| featureMap | <code>\*</code> | + +<a name="module_Validation API_ check element conformance..setConformanceWarnings"></a> + +### Validation API: check element conformance~setConformanceWarnings(db, endpointId, endpointTypeId, endpointClusterId, deviceTypeRefs, cluster, sessionId) ⇒ +Adds warnings to the session notification table during ZAP file imports +for features, attributes, commands, and events that do not correctly conform +within a cluster. + +**Kind**: inner method of [<code>Validation API: check element conformance</code>](#module_Validation API_ check element conformance) +**Returns**: list of warning messages if any, otherwise false + +| Param | Type | +| --- | --- | +| db | <code>\*</code> | +| endpointId | <code>\*</code> | +| endpointTypeId | <code>\*</code> | +| endpointClusterId | <code>\*</code> | +| deviceTypeRefs | <code>\*</code> | +| cluster | <code>\*</code> | +| sessionId | <code>\*</code> | + +<a name="module_Validation API_ Evaluate conformance expressions"></a> + +## Validation API: Evaluate conformance expressions +This module provides utilities for evaluating conformance expressions. + + +* [Validation API: Evaluate conformance expressions](#module_Validation API_ Evaluate conformance expressions) + * [~evaluateConformanceExpression(expression, elementMap)](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression) ⇒ + * [~evaluateBooleanExpression(expr)](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression..evaluateBooleanExpression) + * [~evaluateWithParentheses(expr)](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression..evaluateWithParentheses) + * [~checkMissingTerms(expression, elementMap)](#module_Validation API_ Evaluate conformance expressions..checkMissingTerms) ⇒ + +<a name="module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression"></a> + +### Validation API: Evaluate conformance expressions~evaluateConformanceExpression(expression, elementMap) ⇒ +Evaluate the value of a boolean conformance expression that includes terms and operators. +A term can be an attribute, command, event, feature, or conformance abbreviation. +Operators include AND (&), OR (|), and NOT (!). +The '[]' indicates optional conformance if the expression inside true. +Expression containing comma means otherwise conformance. See spec for details. +Examples of conformance expression: 'A & (!B | C)', 'A & B, [!C]' + +**Kind**: inner method of [<code>Validation API: Evaluate conformance expressions</code>](#module_Validation API_ Evaluate conformance expressions) +**Returns**: 'mandatory', 'optional', 'provisional', or 'notSupported' + +| Param | Type | +| --- | --- | +| expression | <code>\*</code> | +| elementMap | <code>\*</code> | + + +* [~evaluateConformanceExpression(expression, elementMap)](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression) ⇒ + * [~evaluateBooleanExpression(expr)](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression..evaluateBooleanExpression) + * [~evaluateWithParentheses(expr)](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression..evaluateWithParentheses) + +<a name="module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression..evaluateBooleanExpression"></a> + +#### evaluateConformanceExpression~evaluateBooleanExpression(expr) +helper function to evaluate a single boolean expression + +**Kind**: inner method of [<code>evaluateConformanceExpression</code>](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression) + +| Param | Type | +| --- | --- | +| expr | <code>\*</code> | + +<a name="module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression..evaluateWithParentheses"></a> + +#### evaluateConformanceExpression~evaluateWithParentheses(expr) +helper function to process parentheses and evaluate inner expressions first + +**Kind**: inner method of [<code>evaluateConformanceExpression</code>](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression) + +| Param | Type | +| --- | --- | +| expr | <code>\*</code> | + +<a name="module_Validation API_ Evaluate conformance expressions..checkMissingTerms"></a> + +### Validation API: Evaluate conformance expressions~checkMissingTerms(expression, elementMap) ⇒ +Check if any terms in the expression are neither a key in the elementMap nor an abbreviation. +If so, it means the conformance depends on terms with unknown values and changes are not allowed. + +**Kind**: inner method of [<code>Validation API: Evaluate conformance expressions</code>](#module_Validation API_ Evaluate conformance expressions) +**Returns**: all missing terms in an array + +| Param | Type | +| --- | --- | +| expression | <code>\*</code> | +| elementMap | <code>\*</code> | + +<a name="module_Validation API_ Parse conformance data from XML"></a> + +## Validation API: Parse conformance data from XML +This module provides utilities for parsing conformance data from XML into expressions. + + +* [Validation API: Parse conformance data from XML](#module_Validation API_ Parse conformance data from XML) + * [~parseConformanceFromXML(operand)](#module_Validation API_ Parse conformance data from XML..parseConformanceFromXML) ⇒ + * [~parseConformanceRecursively(operand, depth, parentJoinChar)](#module_Validation API_ Parse conformance data from XML..parseConformanceRecursively) ⇒ + +<a name="module_Validation API_ Parse conformance data from XML..parseConformanceFromXML"></a> + +### Validation API: Parse conformance data from XML~parseConformanceFromXML(operand) ⇒ +Parses conformance from XML data. +The conformance could come from features, attributes, commands, or events + +Call recursive helper function to parse conformance only if the conformance exists. +Otherwise, return empty string directly + +An example of parsing the conformance of 'User' device type feature: + +Input operand from xml data: +{ + "$": {"code": "USR", "name": "User"}, + "mandatoryConform": [ + { "andTerm": [ + { + "condition": [{"$": {"name": "Matter"}}], + "orTerm": [ + { "feature": [ + { "$": {"name": "PIN"}}, + { "$": {"name": "RID"}}, + { "$": {"name": "FGP"}}, + { "$": {"name": "FACE"}} + ] + } + ] + } + ] + } + ] +} + +Output conformance string: + "Matter & (PIN | RID | FGP | FACE)" + +**Kind**: inner method of [<code>Validation API: Parse conformance data from XML</code>](#module_Validation API_ Parse conformance data from XML) +**Returns**: The conformance string + +| Param | Type | +| --- | --- | +| operand | <code>\*</code> | + +<a name="module_Validation API_ Parse conformance data from XML..parseConformanceRecursively"></a> + +### Validation API: Parse conformance data from XML~parseConformanceRecursively(operand, depth, parentJoinChar) ⇒ +helper function to parse conformance or an operand in conformance recursively + +The baseLevelTerms variable include terms that can not have nested terms. +When they appear, stop recursing and return the name inside directly + +**Kind**: inner method of [<code>Validation API: Parse conformance data from XML</code>](#module_Validation API_ Parse conformance data from XML) +**Returns**: The conformance string. + +| Param | Type | Default | +| --- | --- | --- | +| operand | <code>\*</code> | | +| depth | <code>\*</code> | <code>0</code> | +| parentJoinChar | <code>\*</code> | | + +<a name="module_Validation API_ Validation APIs"></a> + +## Validation API: Validation APIs +This module provides the APIs for validating inputs to the database, and returning flags indicating if +things were successful or not. + + +* [Validation API: Validation APIs](#module_Validation API_ Validation APIs) + * _static_ + * [.initAsyncValidation()](#module_Validation API_ Validation APIs.initAsyncValidation) + * _inner_ + * [~zigbeeEnforceCommonClusterSpecInit(session)](#module_Validation API_ Validation APIs..zigbeeEnforceCommonClusterSpecInit) ⇒ + * [~zigbeeEnforceCommonClusterSpecCallback(session)](#module_Validation API_ Validation APIs..zigbeeEnforceCommonClusterSpecCallback) + * [~validateAttribute(db, endpointTypeId, attributeRef, clusterRef, zapSessionId)](#module_Validation API_ Validation APIs..validateAttribute) ⇒ + * [~validateEndpoint(db, endpointId)](#module_Validation API_ Validation APIs..validateEndpoint) ⇒ + * [~validateNoDuplicateEndpoints(db, endpointIdentifier, sessionRef)](#module_Validation API_ Validation APIs..validateNoDuplicateEndpoints) ⇒ + * [~validateSpecificAttribute(endpointAttribute, attribute, db, zapSessionId)](#module_Validation API_ Validation APIs..validateSpecificAttribute) ⇒ + * [~validateSpecificEndpoint(endpoint)](#module_Validation API_ Validation APIs..validateSpecificEndpoint) ⇒ + * [~isValidNumberString(value)](#module_Validation API_ Validation APIs..isValidNumberString) ⇒ + * [~isValidHexString(value)](#module_Validation API_ Validation APIs..isValidHexString) ⇒ + * [~isValidDecimalString(value)](#module_Validation API_ Validation APIs..isValidDecimalString) ⇒ + * [~isValidFloat(value)](#module_Validation API_ Validation APIs..isValidFloat) ⇒ + * [~extractFloatValue(value)](#module_Validation API_ Validation APIs..extractFloatValue) ⇒ + * [~extractIntegerValue(value)](#module_Validation API_ Validation APIs..extractIntegerValue) ⇒ + * [~extractBigIntegerValue(value)](#module_Validation API_ Validation APIs..extractBigIntegerValue) ⇒ + * [~isBigInteger(bits)](#module_Validation API_ Validation APIs..isBigInteger) ⇒ + * [~getBoundsInteger(attribute, typeSize, isSigned)](#module_Validation API_ Validation APIs..getBoundsInteger) ⇒ + * [~getTypeRange(typeSize, isSigned, isMin)](#module_Validation API_ Validation APIs..getTypeRange) ⇒ + * [~unsignedToSignedInteger(value, typeSize)](#module_Validation API_ Validation APIs..unsignedToSignedInteger) ⇒ + * [~getIntegerFromAttribute(attribute, typeSize, isSigned)](#module_Validation API_ Validation APIs..getIntegerFromAttribute) ⇒ + * [~getIntegerAttributeSize(db, zapSessionId, attribType)](#module_Validation API_ Validation APIs..getIntegerAttributeSize) ⇒ <code>\*</code> + * [~checkAttributeBoundsInteger(attribute, endpointAttribute, db, zapSessionId)](#module_Validation API_ Validation APIs..checkAttributeBoundsInteger) ⇒ + * [~checkBoundsInteger(defaultValue, min, max)](#module_Validation API_ Validation APIs..checkBoundsInteger) ⇒ + * [~checkAttributeBoundsFloat(attribute, endpointAttribute)](#module_Validation API_ Validation APIs..checkAttributeBoundsFloat) ⇒ + * [~getBoundsFloat(attribute)](#module_Validation API_ Validation APIs..getBoundsFloat) ⇒ + * [~checkBoundsFloat(defaultValue, min, max)](#module_Validation API_ Validation APIs..checkBoundsFloat) ⇒ + +<a name="module_Validation API_ Validation APIs.initAsyncValidation"></a> + +### Validation API: Validation APIs.initAsyncValidation() +Start session specific validation. + +**Kind**: static method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) +<a name="module_Validation API_ Validation APIs..zigbeeEnforceCommonClusterSpecInit"></a> + +### Validation API: Validation APIs~zigbeeEnforceCommonClusterSpecInit(session) ⇒ +Enforce zigbee specific common cluster initialization. + +**Kind**: inner method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) +**Returns**: object + +| Param | Type | +| --- | --- | +| session | <code>\*</code> | + +<a name="module_Validation API_ Validation APIs..zigbeeEnforceCommonClusterSpecCallback"></a> + +### Validation API: Validation APIs~zigbeeEnforceCommonClusterSpecCallback(session) +Enforce zigbee specific common cluster initialization. + +**Kind**: inner method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) + +| Param | Type | +| --- | --- | +| session | <code>\*</code> | + +<a name="module_Validation API_ Validation APIs..validateAttribute"></a> + +### Validation API: Validation APIs~validateAttribute(db, endpointTypeId, attributeRef, clusterRef, zapSessionId) ⇒ +Main attribute validation function. +Returns a promise of an object which stores a list of validation issues. +Such issues as "Invalid type" or "Out of Range". + +**Kind**: inner method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) +**Returns**: Promise of the list of issues + +| Param | Type | Description | +| --- | --- | --- | +| db | <code>\*</code> | db reference | +| endpointTypeId | <code>\*</code> | endpoint reference | +| attributeRef | <code>\*</code> | attribute reference | +| clusterRef | <code>\*</code> | cluster reference | +| zapSessionId | <code>\*</code> | session reference | + +<a name="module_Validation API_ Validation APIs..validateEndpoint"></a> + +### Validation API: Validation APIs~validateEndpoint(db, endpointId) ⇒ +Get issues in an endpoint. + +**Kind**: inner method of [<code>Validation API: Validation APIs</code>](#module_Validation API_ Validation APIs) +**Returns**: object + +| Param | Type | +| --- | --- | +| db | <code>\*</code> | +| endpointId | <code>\*</code> | + <a name="module_Validation API_ Validation APIs..validateNoDuplicateEndpoints"></a> ### Validation API: Validation APIs~validateNoDuplicateEndpoints(db, endpointIdentifier, sessionRef) ⇒ @@ -20123,8 +20212,6 @@ This module provides the APIs for dotdot Loading * [~parseManufacturerData(db, ctx)](#module_Loader API_ Loader APIs..parseManufacturerData) ⇒ * [~parseProfilesData(db, ctx)](#module_Loader API_ Loader APIs..parseProfilesData) ⇒ * [~parseFeatureFlags(db, packageId, featureFlags)](#module_Loader API_ Loader APIs..parseFeatureFlags) ⇒ - * [~parseConformanceFromXML(operand)](#module_Loader API_ Loader APIs..parseConformanceFromXML) ⇒ - * [~parseConformanceRecursively(operand, depth, parentJoinChar)](#module_Loader API_ Loader APIs..parseConformanceRecursively) ⇒ * [~parseUiOptions(db, packageId, featureFlags)](#module_Loader API_ Loader APIs..parseUiOptions) ⇒ * [~parseOptions(db)](#module_Loader API_ Loader APIs..parseOptions) ⇒ * [~parseTextOptions(db, pkgRef, textOptions)](#module_Loader API_ Loader APIs..parseTextOptions) ⇒ @@ -21550,66 +21637,6 @@ Key/velues of the object itself, end up in CODE/LABEL combinations. | packageId | <code>\*</code> | | featureFlags | <code>\*</code> | -<a name="module_Loader API_ Loader APIs..parseConformanceFromXML"></a> - -### Loader API: Loader APIs~parseConformanceFromXML(operand) ⇒ -Parses conformance from XML data. -The conformance could come from features, attributes, commands, or events - -Call recursive helper function to parse conformance only if the conformance exists. -Otherwise, return empty string directly - -An example of parsing the conformance of 'User' device type feature: - -Input operand from xml data: -{ - "$": {"code": "USR", "name": "User"}, - "mandatoryConform": [ - { "andTerm": [ - { - "condition": [{"$": {"name": "Matter"}}], - "orTerm": [ - { "feature": [ - { "$": {"name": "PIN"}}, - { "$": {"name": "RID"}}, - { "$": {"name": "FGP"}}, - { "$": {"name": "FACE"}} - ] - } - ] - } - ] - } - ] -} - -Output conformance string: - "Matter & (PIN | RID | FGP | FACE)" - -**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs) -**Returns**: The conformance string - -| Param | Type | -| --- | --- | -| operand | <code>\*</code> | - -<a name="module_Loader API_ Loader APIs..parseConformanceRecursively"></a> - -### Loader API: Loader APIs~parseConformanceRecursively(operand, depth, parentJoinChar) ⇒ -helper function to parse conformance or an operand in conformance recursively - -The baseLevelTerms variable include terms that can not have nested terms. -When they appear, stop recursing and return the name inside directly - -**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs) -**Returns**: The conformance string. - -| Param | Type | Default | -| --- | --- | --- | -| operand | <code>\*</code> | | -| depth | <code>\*</code> | <code>0</code> | -| parentJoinChar | <code>\*</code> | | - <a name="module_Loader API_ Loader APIs..parseUiOptions"></a> ### Loader API: Loader APIs~parseUiOptions(db, packageId, featureFlags) ⇒ @@ -22037,8 +22064,6 @@ This module provides the APIs for new data model loading * [~parseManufacturerData(db, ctx)](#module_Loader API_ Loader APIs..parseManufacturerData) ⇒ * [~parseProfilesData(db, ctx)](#module_Loader API_ Loader APIs..parseProfilesData) ⇒ * [~parseFeatureFlags(db, packageId, featureFlags)](#module_Loader API_ Loader APIs..parseFeatureFlags) ⇒ - * [~parseConformanceFromXML(operand)](#module_Loader API_ Loader APIs..parseConformanceFromXML) ⇒ - * [~parseConformanceRecursively(operand, depth, parentJoinChar)](#module_Loader API_ Loader APIs..parseConformanceRecursively) ⇒ * [~parseUiOptions(db, packageId, featureFlags)](#module_Loader API_ Loader APIs..parseUiOptions) ⇒ * [~parseOptions(db)](#module_Loader API_ Loader APIs..parseOptions) ⇒ * [~parseTextOptions(db, pkgRef, textOptions)](#module_Loader API_ Loader APIs..parseTextOptions) ⇒ @@ -23464,66 +23489,6 @@ Key/velues of the object itself, end up in CODE/LABEL combinations. | packageId | <code>\*</code> | | featureFlags | <code>\*</code> | -<a name="module_Loader API_ Loader APIs..parseConformanceFromXML"></a> - -### Loader API: Loader APIs~parseConformanceFromXML(operand) ⇒ -Parses conformance from XML data. -The conformance could come from features, attributes, commands, or events - -Call recursive helper function to parse conformance only if the conformance exists. -Otherwise, return empty string directly - -An example of parsing the conformance of 'User' device type feature: - -Input operand from xml data: -{ - "$": {"code": "USR", "name": "User"}, - "mandatoryConform": [ - { "andTerm": [ - { - "condition": [{"$": {"name": "Matter"}}], - "orTerm": [ - { "feature": [ - { "$": {"name": "PIN"}}, - { "$": {"name": "RID"}}, - { "$": {"name": "FGP"}}, - { "$": {"name": "FACE"}} - ] - } - ] - } - ] - } - ] -} - -Output conformance string: - "Matter & (PIN | RID | FGP | FACE)" - -**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs) -**Returns**: The conformance string - -| Param | Type | -| --- | --- | -| operand | <code>\*</code> | - -<a name="module_Loader API_ Loader APIs..parseConformanceRecursively"></a> - -### Loader API: Loader APIs~parseConformanceRecursively(operand, depth, parentJoinChar) ⇒ -helper function to parse conformance or an operand in conformance recursively - -The baseLevelTerms variable include terms that can not have nested terms. -When they appear, stop recursing and return the name inside directly - -**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs) -**Returns**: The conformance string. - -| Param | Type | Default | -| --- | --- | --- | -| operand | <code>\*</code> | | -| depth | <code>\*</code> | <code>0</code> | -| parentJoinChar | <code>\*</code> | | - <a name="module_Loader API_ Loader APIs..parseUiOptions"></a> ### Loader API: Loader APIs~parseUiOptions(db, packageId, featureFlags) ⇒ @@ -23951,8 +23916,6 @@ This module provides the APIs for ZCL/Data-Model loading. * [~parseManufacturerData(db, ctx)](#module_Loader API_ Loader APIs..parseManufacturerData) ⇒ * [~parseProfilesData(db, ctx)](#module_Loader API_ Loader APIs..parseProfilesData) ⇒ * [~parseFeatureFlags(db, packageId, featureFlags)](#module_Loader API_ Loader APIs..parseFeatureFlags) ⇒ - * [~parseConformanceFromXML(operand)](#module_Loader API_ Loader APIs..parseConformanceFromXML) ⇒ - * [~parseConformanceRecursively(operand, depth, parentJoinChar)](#module_Loader API_ Loader APIs..parseConformanceRecursively) ⇒ * [~parseUiOptions(db, packageId, featureFlags)](#module_Loader API_ Loader APIs..parseUiOptions) ⇒ * [~parseOptions(db)](#module_Loader API_ Loader APIs..parseOptions) ⇒ * [~parseTextOptions(db, pkgRef, textOptions)](#module_Loader API_ Loader APIs..parseTextOptions) ⇒ @@ -25378,66 +25341,6 @@ Key/velues of the object itself, end up in CODE/LABEL combinations. | packageId | <code>\*</code> | | featureFlags | <code>\*</code> | -<a name="module_Loader API_ Loader APIs..parseConformanceFromXML"></a> - -### Loader API: Loader APIs~parseConformanceFromXML(operand) ⇒ -Parses conformance from XML data. -The conformance could come from features, attributes, commands, or events - -Call recursive helper function to parse conformance only if the conformance exists. -Otherwise, return empty string directly - -An example of parsing the conformance of 'User' device type feature: - -Input operand from xml data: -{ - "$": {"code": "USR", "name": "User"}, - "mandatoryConform": [ - { "andTerm": [ - { - "condition": [{"$": {"name": "Matter"}}], - "orTerm": [ - { "feature": [ - { "$": {"name": "PIN"}}, - { "$": {"name": "RID"}}, - { "$": {"name": "FGP"}}, - { "$": {"name": "FACE"}} - ] - } - ] - } - ] - } - ] -} - -Output conformance string: - "Matter & (PIN | RID | FGP | FACE)" - -**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs) -**Returns**: The conformance string - -| Param | Type | -| --- | --- | -| operand | <code>\*</code> | - -<a name="module_Loader API_ Loader APIs..parseConformanceRecursively"></a> - -### Loader API: Loader APIs~parseConformanceRecursively(operand, depth, parentJoinChar) ⇒ -helper function to parse conformance or an operand in conformance recursively - -The baseLevelTerms variable include terms that can not have nested terms. -When they appear, stop recursing and return the name inside directly - -**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs) -**Returns**: The conformance string. - -| Param | Type | Default | -| --- | --- | --- | -| operand | <code>\*</code> | | -| depth | <code>\*</code> | <code>0</code> | -| parentJoinChar | <code>\*</code> | | - <a name="module_Loader API_ Loader APIs..parseUiOptions"></a> ### Loader API: Loader APIs~parseUiOptions(db, packageId, featureFlags) ⇒ @@ -25865,8 +25768,6 @@ This module provides the APIs for for common functionality related to loading. * [~parseManufacturerData(db, ctx)](#module_Loader API_ Loader APIs..parseManufacturerData) ⇒ * [~parseProfilesData(db, ctx)](#module_Loader API_ Loader APIs..parseProfilesData) ⇒ * [~parseFeatureFlags(db, packageId, featureFlags)](#module_Loader API_ Loader APIs..parseFeatureFlags) ⇒ - * [~parseConformanceFromXML(operand)](#module_Loader API_ Loader APIs..parseConformanceFromXML) ⇒ - * [~parseConformanceRecursively(operand, depth, parentJoinChar)](#module_Loader API_ Loader APIs..parseConformanceRecursively) ⇒ * [~parseUiOptions(db, packageId, featureFlags)](#module_Loader API_ Loader APIs..parseUiOptions) ⇒ * [~parseOptions(db)](#module_Loader API_ Loader APIs..parseOptions) ⇒ * [~parseTextOptions(db, pkgRef, textOptions)](#module_Loader API_ Loader APIs..parseTextOptions) ⇒ @@ -27292,66 +27193,6 @@ Key/velues of the object itself, end up in CODE/LABEL combinations. | packageId | <code>\*</code> | | featureFlags | <code>\*</code> | -<a name="module_Loader API_ Loader APIs..parseConformanceFromXML"></a> - -### Loader API: Loader APIs~parseConformanceFromXML(operand) ⇒ -Parses conformance from XML data. -The conformance could come from features, attributes, commands, or events - -Call recursive helper function to parse conformance only if the conformance exists. -Otherwise, return empty string directly - -An example of parsing the conformance of 'User' device type feature: - -Input operand from xml data: -{ - "$": {"code": "USR", "name": "User"}, - "mandatoryConform": [ - { "andTerm": [ - { - "condition": [{"$": {"name": "Matter"}}], - "orTerm": [ - { "feature": [ - { "$": {"name": "PIN"}}, - { "$": {"name": "RID"}}, - { "$": {"name": "FGP"}}, - { "$": {"name": "FACE"}} - ] - } - ] - } - ] - } - ] -} - -Output conformance string: - "Matter & (PIN | RID | FGP | FACE)" - -**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs) -**Returns**: The conformance string - -| Param | Type | -| --- | --- | -| operand | <code>\*</code> | - -<a name="module_Loader API_ Loader APIs..parseConformanceRecursively"></a> - -### Loader API: Loader APIs~parseConformanceRecursively(operand, depth, parentJoinChar) ⇒ -helper function to parse conformance or an operand in conformance recursively - -The baseLevelTerms variable include terms that can not have nested terms. -When they appear, stop recursing and return the name inside directly - -**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs) -**Returns**: The conformance string. - -| Param | Type | Default | -| --- | --- | --- | -| operand | <code>\*</code> | | -| depth | <code>\*</code> | <code>0</code> | -| parentJoinChar | <code>\*</code> | | - <a name="module_Loader API_ Loader APIs..parseUiOptions"></a> ### Loader API: Loader APIs~parseUiOptions(db, packageId, featureFlags) ⇒ diff --git a/src-electron/db/query-endpoint-type.js b/src-electron/db/query-endpoint-type.js index 81389dac0c1c5ea92dad74a8f3bbb1b8ed154794..7c4944be587ea4a06f4f499f780239740016b061 100644 --- a/src-electron/db/query-endpoint-type.js +++ b/src-electron/db/query-endpoint-type.js @@ -23,6 +23,9 @@ const dbApi = require('./db-api.js') const dbMapping = require('./db-mapping.js') +const queryAttribute = require('./query-attribute') +const queryCommand = require('./query-command') +const queryEvent = require('./query-event') /** * Promise to delete an endpoint type. @@ -769,6 +772,39 @@ async function selectEndpointTypeClusterFromEndpointIdentifierAndAttributeRef( return dbMapping.map.endpointTypeCluster(etc) } +/** + * Get all attributes, commands and events in an endpoint type cluster. + * @param {*} db + * @param {*} endpointTypeClusterId + * @param {*} deviceTypeClusterId + * @returns elements object containing all attributes, commands and events + * in an endpoint type cluster + */ +async function getEndpointTypeElements( + db, + endpointTypeClusterId, + deviceTypeClusterId +) { + let [attributes, commands, events] = await Promise.all([ + queryAttribute.selectAttributesByEndpointTypeClusterIdAndDeviceTypeClusterId( + db, + endpointTypeClusterId, + deviceTypeClusterId + ), + queryCommand.selectCommandsByEndpointTypeClusterIdAndDeviceTypeClusterId( + db, + endpointTypeClusterId, + deviceTypeClusterId + ), + queryEvent.selectEventsByEndpointTypeClusterIdAndDeviceTypeClusterId( + db, + endpointTypeClusterId, + deviceTypeClusterId + ) + ]) + return { attributes, commands, events } +} + exports.deleteEndpointType = deleteEndpointType exports.selectAllEndpointTypes = selectAllEndpointTypes exports.selectEndpointTypeIds = selectEndpointTypeIds @@ -790,3 +826,4 @@ exports.selectEndpointTypeAttributeFromEndpointTypeClusterId = selectEndpointTypeAttributeFromEndpointTypeClusterId exports.selectEndpointTypeClusterFromEndpointIdentifierAndAttributeRef = selectEndpointTypeClusterFromEndpointIdentifierAndAttributeRef +exports.getEndpointTypeElements = getEndpointTypeElements diff --git a/src-electron/db/query-feature.js b/src-electron/db/query-feature.js index 87d28ec363e6d5de0dc51ea3cb96d1fcd7167191..256f43a2c87f77cb555b98b23bd832ff68dd827c 100644 --- a/src-electron/db/query-feature.js +++ b/src-electron/db/query-feature.js @@ -22,9 +22,6 @@ */ const dbApi = require('./db-api') const dbMapping = require('./db-mapping') -const queryAttribute = require('./query-attribute') -const queryCommand = require('./query-command') -const queryEvent = require('./query-event') /** * Get all device type features associated with a list of device type refs and an endpoint. @@ -138,485 +135,6 @@ async function getFeaturesByDeviceTypeRefs( return Object.values(result) } -/** - * Evaluate the value of a boolean conformance expression that includes terms and operators. - * A term can be an attribute, command, event, feature, or conformance abbreviation. - * Operators include AND (&), OR (|), and NOT (!). - * The '[]' indicates optional conformance if the expression inside true. - * Expression containing comma means otherwise conformance. See spec for details. - * Examples of conformance expression: 'A & (!B | C)', 'A & B, [!C]' - * - * @export - * @param {*} expression - * @param {*} elementMap - * @returns 'mandatory', 'optional', 'provisional', or 'notSupported' - */ -function evaluateConformanceExpression(expression, elementMap) { - /** - * helper function to evaluate a single boolean expression - * @param {*} expr - */ - function evaluateBooleanExpression(expr) { - // Replace terms with their actual values from elementMap - expr = expr.replace(/[A-Za-z][A-Za-z0-9_]*/g, (term) => { - if (elementMap[term]) { - return 'true' - } else { - return 'false' - } - }) - - // Evaluate NOT (!) operators - expr = expr.replace(/!true/g, 'false').replace(/!false/g, 'true') - - // Evaluate AND (&) and OR (|) operators by eval() function - return eval(expr) - } - - /** - * helper function to process parentheses and evaluate inner expressions first - * @param {*} expr - */ - function evaluateWithParentheses(expr) { - while (expr.includes('(')) { - expr = expr.replace(/\([^()]+\)/g, (terms) => - evaluateBooleanExpression(terms.slice(1, -1)) - ) - } - return evaluateBooleanExpression(expr) - } - - // Check ',' for otherwise conformance first. - // Split the expression by ',' and evaluate each part in sequence - let parts = expression.split(',') - // if any term is desc, the conformance is too complex to parse - for (let part of parts) { - let terms = part.match(/[A-Za-z][A-Za-z0-9_]*/g) - if (terms && terms.includes('desc')) { - return 'desc' - } - } - for (let part of parts) { - if (part.includes('[') && part.includes(']')) { - // Extract and evaluate the content inside '[]' - let optionalExpr = part.match(/\[(.*?)\]/)[1] - let optionalResult = evaluateWithParentheses(optionalExpr) - if (optionalResult) { - return 'optional' - } else { - return 'notSupported' - } - } else { - part = part.trim() - if (part == 'M') { - return 'mandatory' - } else if (part == 'O') { - return 'optional' - } else if (part == 'D' || part == 'X') { - return 'notSupported' - } else if (part == 'P') { - return 'provisional' - } else { - // Evaluate the part with parentheses if needed - let result = evaluateWithParentheses(part) - if (result) return 'mandatory' - // if the mandatory part is false, go to the next part - } - } - } - - // If none of the parts are true and no optional part was valid, return 'notSupported' - return 'notSupported' -} - -/** - * Check if any terms in the expression are neither a key in the elementMap nor an abbreviation. - * If so, it means the conformance depends on terms with unknown values and changes are not allowed. - * - * @param {*} expression - * @param {*} elementMap - * @returns all missing terms in an array - */ -function checkMissingTerms(expression, elementMap) { - let terms = expression.match(/[A-Za-z][A-Za-z0-9_]*/g) - let missingTerms = [] - let abbreviations = ['M', 'O', 'P', 'D', 'X'] - for (let term of terms) { - if (!(term in elementMap) && !abbreviations.includes(term)) { - missingTerms.push(term) - } - } - return missingTerms -} - -/** - * Filter an array of elements by if any element has conformance containing the term 'desc'. - * - * @export - * @param {*} elements - * @returns elements with conformance containing 'desc' - */ -function filterElementsContainingDesc(elements) { - return elements.filter((element) => { - let terms = element.conformance.match(/[A-Za-z][A-Za-z0-9_]*/g) - return terms && terms.includes('desc') - }) -} - -/** - * - * @export - * @param {*} elements - * @param {*} featureCode - * @returns elements with conformance containing 'desc' and the feature code - */ -function filterRelatedDescElements(elements, featureCode) { - return elements.filter((element) => { - let terms = element.conformance.match(/[A-Za-z][A-Za-z0-9_]*/g) - return terms && terms.includes('desc') && terms.includes(featureCode) - }) -} - -/** - * Generate a warning message after processing conformance of the updated device type feature. - * Set flags to decide whether to show warnings or disable changes in the frontend. - * - * @param {*} featureData - * @param {*} endpointId - * @param {*} elementMap - * @param {*} featureMap - * @param {*} descElements - * @returns warning message array, disableChange flag, and displayWarning flag - */ -function generateWarningMessage( - featureData, - endpointId, - featureMap, - elementMap = {}, - descElements = {} -) { - let featureName = featureData.name - let added = featureMap[featureData.code] ? true : false - let deviceTypeNames = featureData.deviceTypes.join(', ') - let result = { - warningMessage: '', - disableChange: true, - displayWarning: true - } - result.warningMessage = [] - - let warningPrefix = - `⚠Check Feature Compliance on endpoint: ${endpointId}, cluster: ${featureData.cluster}, ` + - `feature: ${featureName} (bit ${featureData.bit} in featureMap attribute)` - - let missingTerms = [] - if (Object.keys(elementMap).length > 0) { - missingTerms = checkMissingTerms(featureData.conformance, elementMap) - if (missingTerms.length > 0) { - let missingTermsString = missingTerms.join(', ') - result.warningMessage.push( - warningPrefix + - ' cannot be enabled as its conformance depends on non device type features ' + - missingTermsString + - ' with unknown values' - ) - } - } - - if ( - (descElements.attributes && descElements.attributes.length > 0) || - (descElements.commands && descElements.commands.length > 0) || - (descElements.events && descElements.events.length > 0) - ) { - let attributeNames = descElements.attributes - .map((attr) => attr.name) - .join(', ') - let commandNames = descElements.commands - .map((command) => command.name) - .join(', ') - let eventNames = descElements.events.map((event) => event.name).join(', ') - result.warningMessage.push( - warningPrefix + - ' cannot be enabled as ' + - (attributeNames ? 'attribute ' + attributeNames : '') + - (attributeNames && commandNames ? ', ' : '') + - (commandNames ? 'command ' + commandNames : '') + - ((attributeNames || commandNames) && eventNames ? ', ' : '') + - (eventNames ? 'event ' + eventNames : '') + - ' depend on the feature and their conformance are too complex to parse.' - ) - } - - if ( - missingTerms.length == 0 && - (Object.keys(descElements).length == 0 || - (descElements.attributes.length == 0 && - descElements.commands.length == 0 && - descElements.events.length == 0)) - ) { - let conformance = evaluateConformanceExpression( - featureData.conformance, - featureMap - ) - // if no missing terms and no desc elements, enable the feature change - result.disableChange = false - result.displayWarning = false - // in this case only 1 warning message is needed - result.warningMessage = '' - if (conformance == 'notSupported') { - result.warningMessage = - warningPrefix + - ' should be disabled, as it is not supported for device type: ' + - deviceTypeNames - result.displayWarning = added - } - if (conformance == 'provisional') { - result.warningMessage = - warningPrefix + - ' is enabled, but it is still provisional for device type: ' + - deviceTypeNames - result.displayWarning = added - } - if (conformance == 'mandatory') { - result.warningMessage = - warningPrefix + - ' should be enabled, as it is mandatory for device type: ' + - deviceTypeNames - result.displayWarning = !added - } - } - - return result -} - -/** - * Check if elements need to be updated for correct conformance if featureData provided. - * Otherwise, check if elements are required or unsupported by their conformance. - * - * @export - * @param {*} elements - * @param {*} featureMap - * @param {*} featureData - * @param {*} endpointId - * @returns attributes, commands, and events to update, with warnings if featureData provided; - * required and unsupported attributes, commands, and events, with warnings if not. - */ -function checkElementConformance( - elements, - featureMap, - featureData = null, - endpointId = null -) { - let { attributes, commands, events } = elements - let featureCode = featureData ? featureData.code : '' - - // create a map of element names/codes to their enabled status - let elementMap = { ...featureMap } - attributes.forEach((attribute) => { - elementMap[attribute.name] = attribute.included - }) - commands.forEach((command) => { - elementMap[command.name] = command.isEnabled - }) - events.forEach((event) => { - elementMap[event.name] = event.included - }) - elementMap['Matter'] = 1 - elementMap['Zigbee'] = 0 - - let warningInfo = {} - if (featureData != null) { - let descElements = {} - descElements.attributes = filterRelatedDescElements(attributes, featureCode) - descElements.commands = filterRelatedDescElements(commands, featureCode) - descElements.events = filterRelatedDescElements(events, featureCode) - - warningInfo = generateWarningMessage( - featureData, - endpointId, - featureMap, - elementMap, - descElements - ) - - if (warningInfo.disableChange) { - return { - ...warningInfo, - attributesToUpdate: [], - commandsToUpdate: [], - eventsToUpdate: [] - } - } - } - - // check element conformance for if they need update or are required - let attributesToUpdate = featureData - ? filterElementsToUpdate(attributes, elementMap, featureCode) - : filterRequiredElements(attributes, elementMap, featureMap) - let commandsToUpdate = featureData - ? filterElementsToUpdate(commands, elementMap, featureCode) - : filterRequiredElements(commands, elementMap, featureMap) - let eventsToUpdate = featureData - ? filterElementsToUpdate(events, elementMap, featureCode) - : filterRequiredElements(events, elementMap, featureMap) - - let result = { - attributesToUpdate: attributesToUpdate, - commandsToUpdate: commandsToUpdate, - eventsToUpdate: eventsToUpdate, - elementMap: elementMap - } - return featureData ? { ...warningInfo, ...result } : result -} - -/** - * Return attributes, commands, or events to be updated satisfying: - * (1) its conformance includes feature code of the updated feature - * (2) it has mandatory conformance but it is not enabled, OR, - * it is has notSupported conformance but it is enabled - * - * @param {*} elements - * @param {*} elementMap - * @param {*} featureCode - * @returns elements that should be updated - */ -function filterElementsToUpdate(elements, elementMap, featureCode) { - let elementsToUpdate = [] - elements - .filter((element) => element.conformance.includes(featureCode)) - .forEach((element) => { - let conformance = evaluateConformanceExpression( - element.conformance, - elementMap - ) - if ( - conformance == 'mandatory' && - (!elementMap[element.name] || elementMap[element.name] == 0) - ) { - element.value = true - elementsToUpdate.push(element) - } - if (conformance == 'notSupported' && elementMap[element.name]) { - element.value = false - elementsToUpdate.push(element) - } - }) - return elementsToUpdate -} - -/** - * Get warnings for element requirements that are outdated after a feature update. - * - * @param {*} featureData - * @param {*} elements - * @param {*} elementMap - * @returns array of outdated element warnings - */ -function getOutdatedElementWarning(featureData, elements, elementMap) { - let outdatedWarnings = [] - - /** - * Build substrings of outdated warnings and add to returned array if: - * (1) the element conformance includes the feature code - * (2) the element conformance has changed after the feature update - * - * @param {*} elementType - */ - function processElements(elementType) { - elements[elementType].forEach((element) => { - if (element.conformance.includes(featureData.code)) { - let newConform = evaluateConformanceExpression( - element.conformance, - elementMap - ) - let oldMap = { ...elementMap } - oldMap[featureData.code] = !oldMap[featureData.code] - let oldConform = evaluateConformanceExpression( - element.conformance, - oldMap - ) - if (newConform != oldConform) { - let pattern = `${element.name} has mandatory conformance to ${element.conformance} and should be` - outdatedWarnings.push(pattern) - } - } - }) - } - - processElements('attributes') - processElements('commands') - processElements('events') - - return outdatedWarnings -} - -/** - * Filter required and unsupported elements based on their conformance and generate warnings. - * An element is required if it conforms to element(s) in elementMap and has 'mandatory' conform. - * An element is unsupported if it conforms to element(s) in elementMap and has 'notSupported' conform. - * - * @param {*} elements - * @param {*} elementMap - * @param {*} featureMap - * @returns required and not supported elements with warnings - */ -function filterRequiredElements(elements, elementMap, featureMap) { - let requiredElements = { - required: {}, - notSupported: {} - } - elements.forEach((element) => { - let conformance = evaluateConformanceExpression( - element.conformance, - elementMap - ) - let expression = element.conformance - let terms = expression ? expression.match(/[A-Za-z][A-Za-z0-9_]*/g) : [] - let featureTerms = terms - .filter((term) => term in featureMap) - .map( - (term) => - `feature: ${term} is ${featureMap[term] ? 'enabled' : 'disabled'}` - ) - .join(', ') - let elementTerms = terms - .filter((term) => !(term in featureMap)) - .map( - (term) => - `element: ${term} is ${elementMap[term] ? 'enabled' : 'disabled'}` - ) - .join(', ') - let combinedTerms = [featureTerms, elementTerms].filter(Boolean).join(', ') - let conformToElement = terms.some((term) => - Object.keys(elementMap).includes(term) - ) - - if (conformToElement) { - let suggestedState = '' - if (conformance == 'mandatory') { - suggestedState = 'enabled' - } - if (conformance == 'notSupported') { - suggestedState = 'disabled' - } - - // generate warning message for required and unsupported elements - element.warningMessage = - `${element.name} has mandatory conformance to ${element.conformance} ` + - `and should be ${suggestedState} when ` + - combinedTerms + - '.' - if (conformance == 'mandatory') { - requiredElements.required[element.id] = element.warningMessage - } - if (conformance == 'notSupported') { - requiredElements.notSupported[element.id] = element.warningMessage - } - } - }) - return requiredElements -} - /** * Check if any non-empty conformance data exist in ATTRIBUTE, COMMAND, * and DEVICE_TYPE_FEATURE table. @@ -654,45 +172,5 @@ async function checkIfConformanceDataExist(db) { } } -/** - * Get all attributes, commands and events in an endpoint type cluster. - * @param {*} db - * @param {*} endpointTypeClusterId - * @param {*} deviceTypeClusterId - * @returns elements object containing all attributes, commands and events - * in an endpoint type cluster - */ -async function getEndpointTypeElements( - db, - endpointTypeClusterId, - deviceTypeClusterId -) { - let [attributes, commands, events] = await Promise.all([ - queryAttribute.selectAttributesByEndpointTypeClusterIdAndDeviceTypeClusterId( - db, - endpointTypeClusterId, - deviceTypeClusterId - ), - queryCommand.selectCommandsByEndpointTypeClusterIdAndDeviceTypeClusterId( - db, - endpointTypeClusterId, - deviceTypeClusterId - ), - queryEvent.selectEventsByEndpointTypeClusterIdAndDeviceTypeClusterId( - db, - endpointTypeClusterId, - deviceTypeClusterId - ) - ]) - return { attributes, commands, events } -} - exports.getFeaturesByDeviceTypeRefs = getFeaturesByDeviceTypeRefs -exports.checkElementConformance = checkElementConformance -exports.generateWarningMessage = generateWarningMessage -exports.evaluateConformanceExpression = evaluateConformanceExpression -exports.filterElementsContainingDesc = filterElementsContainingDesc -exports.filterRelatedDescElements = filterRelatedDescElements exports.checkIfConformanceDataExist = checkIfConformanceDataExist -exports.getOutdatedElementWarning = getOutdatedElementWarning -exports.getEndpointTypeElements = getEndpointTypeElements diff --git a/src-electron/importexport/import-json.js b/src-electron/importexport/import-json.js index f686eb64d1dbf17d343abd13fd0fa0fc0f7747b2..d861172f794df5deeb03199c31b359157d3790ce 100644 --- a/src-electron/importexport/import-json.js +++ b/src-electron/importexport/import-json.js @@ -34,8 +34,7 @@ const queryZcl = require('../db/query-zcl.js') const querySessionNotice = require('../db/query-session-notification.js') const queryDeviceType = require('../db/query-device-type.js') const queryCommand = require('../db/query-command.js') -const queryConfig = require('../db/query-config.js') -const queryFeature = require('../db/query-feature.js') +const conformChecker = require('../validation/conformance-checker.js') const zclLoader = require('../zcl/zcl-loader.js') const generationEngine = require('../generator/generation-engine') @@ -505,7 +504,7 @@ async function importClusters( sessionId ) - let clusterConformWarnings = await setConformanceWarnings( + let clusterConformWarnings = await conformChecker.setConformanceWarnings( db, endpointId, endpointTypeId, @@ -1955,138 +1954,6 @@ async function jsonDataLoader( } } -/** - * Adds warnings to the session notification table during ZAP file imports - * for features, attributes, commands, and events that do not correctly conform - * within a cluster. - * @param {*} db - * @param {*} endpointId - * @param {*} endpointTypeId - * @param {*} endpointClusterId - * @param {*} deviceTypeRefs - * @param {*} cluster - * @param {*} sessionId - * @returns list of warning messages if any, otherwise false - */ -async function setConformanceWarnings( - db, - endpointId, - endpointTypeId, - endpointClusterId, - deviceTypeRefs, - cluster, - sessionId -) { - let deviceTypeFeatures = await queryFeature.getFeaturesByDeviceTypeRefs( - db, - deviceTypeRefs, - endpointTypeId - ) - let clusterFeatures = deviceTypeFeatures.filter( - (feature) => feature.endpointTypeClusterId == endpointClusterId - ) - - if (clusterFeatures.length > 0) { - let deviceTypeClusterId = clusterFeatures[0].deviceTypeClusterId - let endpointTypeElements = await queryFeature.getEndpointTypeElements( - db, - endpointClusterId, - deviceTypeClusterId - ) - - let featureMapVal = clusterFeatures[0].featureMapValue - let featureMap = {} - for (let feature of clusterFeatures) { - let bit = feature.bit - let bitVal = (featureMapVal & (1 << bit)) >> bit - featureMap[feature.code] = bitVal - } - - // get elements that should be mandatory or unsupported based on conformance - let requiredElements = queryFeature.checkElementConformance( - endpointTypeElements, - featureMap - ) - - let warnings = [] - // set warnings for each feature in the cluster - for (const featureData of clusterFeatures) { - let warningInfo = queryFeature.generateWarningMessage( - featureData, - endpointId, - featureMap - ) - if (warningInfo.displayWarning && warningInfo.warningMessage) { - warnings.push(warningInfo.warningMessage) - } - } - - let contextMessage = `⚠Check Feature Compliance on endpoint: ${endpointId}, cluster: ${cluster.name}, ` - - /* If unsupported elements are enabled or required elements are disabled, - they are considered non-conforming. A corresponding warning message will be - generated and added to the warnings array. */ - const filterNonConformElements = ( - elementType, - requiredMap, - notSupportedMap, - elements - ) => { - let elementMap = {} - elements.forEach((element) => { - elementType == 'command' - ? (elementMap[element.id] = element.isEnabled) - : (elementMap[element.id] = element.included) - }) - Object.entries(requiredMap).forEach(([id, message]) => { - if (!(id in elementMap) || !elementMap[id]) { - warnings.push(contextMessage + elementType + ': ' + message) - } - }) - Object.entries(notSupportedMap).forEach(([id, message]) => { - if (id in elementMap && elementMap[id]) { - warnings.push(contextMessage + elementType + ': ' + message) - } - }) - } - - filterNonConformElements( - 'attribute', - requiredElements.attributesToUpdate.required, - requiredElements.attributesToUpdate.notSupported, - endpointTypeElements.attributes - ) - filterNonConformElements( - 'command', - requiredElements.commandsToUpdate.required, - requiredElements.commandsToUpdate.notSupported, - endpointTypeElements.commands - ) - filterNonConformElements( - 'event', - requiredElements.eventsToUpdate.required, - requiredElements.eventsToUpdate.notSupported, - endpointTypeElements.events - ) - - // set warnings in the session notification table - if (warnings.length > 0) { - for (const warning of warnings) { - await querySessionNotice.setNotification( - db, - 'WARNING', - warning, - sessionId, - 1, - 0 - ) - } - return warnings - } - } - return false -} - /** * Generate warning messages for enabled provisional clusters within an endpoint. * diff --git a/src-electron/rest/user-data.js b/src-electron/rest/user-data.js index cb2370243129b978fd3bb20707dc6df41e798262..d9f01af03a7cb88bb132def2fc834b98d9caed9c 100644 --- a/src-electron/rest/user-data.js +++ b/src-electron/rest/user-data.js @@ -41,6 +41,7 @@ const restApi = require('../../src-shared/rest-api.js') const zclLoader = require('../zcl/zcl-loader.js') const dbEnum = require('../../src-shared/db-enum.js') const { StatusCodes } = require('http-status-codes') +const conformChecker = require('../validation/conformance-checker.js') /** * HTTP GET: session key values @@ -110,13 +111,13 @@ function httpPostCheckConformOnFeatureUpdate(db) { let { featureData, featureMap, endpointId } = request.body let { endpointTypeClusterId, deviceTypeClusterId } = featureData - let elements = await queryFeature.getEndpointTypeElements( + let elements = await queryEndpointType.getEndpointTypeElements( db, endpointTypeClusterId, deviceTypeClusterId ) // check element conform and return elements that need to be updated - let result = queryFeature.checkElementConformance( + let result = conformChecker.checkElementConformance( elements, featureMap, featureData, @@ -131,7 +132,7 @@ function httpPostCheckConformOnFeatureUpdate(db) { ) // do not set element warning if feature change disabled if (!result.disableChange) { - let outdatedWarnings = queryFeature.getOutdatedElementWarning( + let outdatedWarnings = conformChecker.getOutdatedElementWarning( featureData, elements, result.elementMap @@ -158,12 +159,12 @@ function httpGetRequiredElements(db) { request.query.data ) featureMap = JSON.parse(featureMap) - let endpointTypeElements = await queryFeature.getEndpointTypeElements( + let endpointTypeElements = await queryEndpointType.getEndpointTypeElements( db, endpointTypeClusterId, deviceTypeClusterId ) - let result = queryFeature.checkElementConformance( + let result = conformChecker.checkElementConformance( endpointTypeElements, featureMap ) diff --git a/src-electron/validation/conformance-checker.js b/src-electron/validation/conformance-checker.js new file mode 100644 index 0000000000000000000000000000000000000000..c0630ce91910d0b407df49ec5df90b89b3cf1389 --- /dev/null +++ b/src-electron/validation/conformance-checker.js @@ -0,0 +1,522 @@ +/** + * + * Copyright (c) 2025 Silicon Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This module provides utilities for checking if elements meet conformance requirements + * and generate warnings for non-conformance. + * + * @module Validation API: check element conformance + */ + +const conformEvaluator = require('./conformance-expression-evaluator') +const queryFeature = require('../db/query-feature') +const querySessionNotice = require('../db/query-session-notification') +const queryEndpointType = require('../db/query-endpoint-type') + +/** + * + * @export + * @param {*} elements + * @param {*} featureCode + * @returns elements with conformance containing 'desc' and the feature code + */ +function filterRelatedDescElements(elements, featureCode) { + return elements.filter((element) => { + let terms = element.conformance.match(/[A-Za-z][A-Za-z0-9_]*/g) + return terms && terms.includes('desc') && terms.includes(featureCode) + }) +} + +/** + * Generate a warning message after processing conformance of the updated device type feature. + * Set flags to decide whether to show warnings or disable changes in the frontend. + * + * @param {*} featureData + * @param {*} endpointId + * @param {*} elementMap + * @param {*} featureMap + * @param {*} descElements + * @returns warning message array, disableChange flag, and displayWarning flag + */ +function generateWarningMessage( + featureData, + endpointId, + featureMap, + elementMap = {}, + descElements = {} +) { + let featureName = featureData.name + let added = featureMap[featureData.code] ? true : false + let deviceTypeNames = featureData.deviceTypes.join(', ') + let result = { + warningMessage: '', + disableChange: true, + displayWarning: true + } + result.warningMessage = [] + + let warningPrefix = + `⚠Check Feature Compliance on endpoint: ${endpointId}, cluster: ${featureData.cluster}, ` + + `feature: ${featureName} (bit ${featureData.bit} in featureMap attribute)` + + let missingTerms = [] + if (Object.keys(elementMap).length > 0) { + missingTerms = conformEvaluator.checkMissingTerms( + featureData.conformance, + elementMap + ) + if (missingTerms.length > 0) { + let missingTermsString = missingTerms.join(', ') + result.warningMessage.push( + warningPrefix + + ' cannot be enabled as its conformance depends on non device type features ' + + missingTermsString + + ' with unknown values' + ) + } + } + + if ( + (descElements.attributes && descElements.attributes.length > 0) || + (descElements.commands && descElements.commands.length > 0) || + (descElements.events && descElements.events.length > 0) + ) { + let attributeNames = descElements.attributes + .map((attr) => attr.name) + .join(', ') + let commandNames = descElements.commands + .map((command) => command.name) + .join(', ') + let eventNames = descElements.events.map((event) => event.name).join(', ') + result.warningMessage.push( + warningPrefix + + ' cannot be enabled as ' + + (attributeNames ? 'attribute ' + attributeNames : '') + + (attributeNames && commandNames ? ', ' : '') + + (commandNames ? 'command ' + commandNames : '') + + ((attributeNames || commandNames) && eventNames ? ', ' : '') + + (eventNames ? 'event ' + eventNames : '') + + ' depend on the feature and their conformance are too complex to parse.' + ) + } + + if ( + missingTerms.length == 0 && + (Object.keys(descElements).length == 0 || + (descElements.attributes.length == 0 && + descElements.commands.length == 0 && + descElements.events.length == 0)) + ) { + let conformance = conformEvaluator.evaluateConformanceExpression( + featureData.conformance, + featureMap + ) + // if no missing terms and no desc elements, enable the feature change + result.disableChange = false + result.displayWarning = false + // in this case only 1 warning message is needed + result.warningMessage = '' + if (conformance == 'notSupported') { + result.warningMessage = + warningPrefix + + ' should be disabled, as it is not supported for device type: ' + + deviceTypeNames + result.displayWarning = added + } + if (conformance == 'provisional') { + result.warningMessage = + warningPrefix + + ' is enabled, but it is still provisional for device type: ' + + deviceTypeNames + result.displayWarning = added + } + if (conformance == 'mandatory') { + result.warningMessage = + warningPrefix + + ' should be enabled, as it is mandatory for device type: ' + + deviceTypeNames + result.displayWarning = !added + } + } + + return result +} + +/** + * Check if elements need to be updated for correct conformance if featureData provided. + * Otherwise, check if elements are required or unsupported by their conformance. + * + * @export + * @param {*} elements + * @param {*} featureMap + * @param {*} featureData + * @param {*} endpointId + * @returns attributes, commands, and events to update, with warnings if featureData provided; + * required and unsupported attributes, commands, and events, with warnings if not. + */ +function checkElementConformance( + elements, + featureMap, + featureData = null, + endpointId = null +) { + let { attributes, commands, events } = elements + let featureCode = featureData ? featureData.code : '' + + // create a map of element names/codes to their enabled status + let elementMap = { ...featureMap } + attributes.forEach((attribute) => { + elementMap[attribute.name] = attribute.included + }) + commands.forEach((command) => { + elementMap[command.name] = command.isEnabled + }) + events.forEach((event) => { + elementMap[event.name] = event.included + }) + elementMap['Matter'] = 1 + elementMap['Zigbee'] = 0 + + let warningInfo = {} + if (featureData != null) { + let descElements = {} + descElements.attributes = filterRelatedDescElements(attributes, featureCode) + descElements.commands = filterRelatedDescElements(commands, featureCode) + descElements.events = filterRelatedDescElements(events, featureCode) + + warningInfo = generateWarningMessage( + featureData, + endpointId, + featureMap, + elementMap, + descElements + ) + + if (warningInfo.disableChange) { + return { + ...warningInfo, + attributesToUpdate: [], + commandsToUpdate: [], + eventsToUpdate: [] + } + } + } + + // check element conformance for if they need update or are required + let attributesToUpdate = featureData + ? filterElementsToUpdate(attributes, elementMap, featureCode) + : filterRequiredElements(attributes, elementMap, featureMap) + let commandsToUpdate = featureData + ? filterElementsToUpdate(commands, elementMap, featureCode) + : filterRequiredElements(commands, elementMap, featureMap) + let eventsToUpdate = featureData + ? filterElementsToUpdate(events, elementMap, featureCode) + : filterRequiredElements(events, elementMap, featureMap) + + let result = { + attributesToUpdate: attributesToUpdate, + commandsToUpdate: commandsToUpdate, + eventsToUpdate: eventsToUpdate, + elementMap: elementMap + } + return featureData ? { ...warningInfo, ...result } : result +} + +/** + * Return attributes, commands, or events to be updated satisfying: + * (1) its conformance includes feature code of the updated feature + * (2) it has mandatory conformance but it is not enabled, OR, + * it is has notSupported conformance but it is enabled + * + * @param {*} elements + * @param {*} elementMap + * @param {*} featureCode + * @returns elements that should be updated + */ +function filterElementsToUpdate(elements, elementMap, featureCode) { + let elementsToUpdate = [] + elements + .filter((element) => element.conformance.includes(featureCode)) + .forEach((element) => { + let conformance = conformEvaluator.evaluateConformanceExpression( + element.conformance, + elementMap + ) + if ( + conformance == 'mandatory' && + (!elementMap[element.name] || elementMap[element.name] == 0) + ) { + element.value = true + elementsToUpdate.push(element) + } + if (conformance == 'notSupported' && elementMap[element.name]) { + element.value = false + elementsToUpdate.push(element) + } + }) + return elementsToUpdate +} + +/** + * Get warnings for element requirements that are outdated after a feature update. + * + * @param {*} featureData + * @param {*} elements + * @param {*} elementMap + * @returns array of outdated element warnings + */ +function getOutdatedElementWarning(featureData, elements, elementMap) { + let outdatedWarnings = [] + + /** + * Build substrings of outdated warnings and add to returned array if: + * (1) the element conformance includes the feature code + * (2) the element conformance has changed after the feature update + * + * @param {*} elementType + */ + function processElements(elementType) { + elements[elementType].forEach((element) => { + if (element.conformance.includes(featureData.code)) { + let newConform = conformEvaluator.evaluateConformanceExpression( + element.conformance, + elementMap + ) + let oldMap = { ...elementMap } + oldMap[featureData.code] = !oldMap[featureData.code] + let oldConform = conformEvaluator.evaluateConformanceExpression( + element.conformance, + oldMap + ) + if (newConform != oldConform) { + let pattern = `${element.name} has mandatory conformance to ${element.conformance} and should be` + outdatedWarnings.push(pattern) + } + } + }) + } + + processElements('attributes') + processElements('commands') + processElements('events') + + return outdatedWarnings +} + +/** + * Filter required and unsupported elements based on their conformance and generate warnings. + * An element is required if it conforms to element(s) in elementMap and has 'mandatory' conform. + * An element is unsupported if it conforms to element(s) in elementMap and has 'notSupported' conform. + * + * @param {*} elements + * @param {*} elementMap + * @param {*} featureMap + * @returns required and not supported elements with warnings + */ +function filterRequiredElements(elements, elementMap, featureMap) { + let requiredElements = { + required: {}, + notSupported: {} + } + elements.forEach((element) => { + let conformance = conformEvaluator.evaluateConformanceExpression( + element.conformance, + elementMap + ) + let expression = element.conformance + let terms = expression ? expression.match(/[A-Za-z][A-Za-z0-9_]*/g) : [] + let featureTerms = terms + .filter((term) => term in featureMap) + .map( + (term) => + `feature: ${term} is ${featureMap[term] ? 'enabled' : 'disabled'}` + ) + .join(', ') + let elementTerms = terms + .filter((term) => !(term in featureMap)) + .map( + (term) => + `element: ${term} is ${elementMap[term] ? 'enabled' : 'disabled'}` + ) + .join(', ') + let combinedTerms = [featureTerms, elementTerms].filter(Boolean).join(', ') + let conformToElement = terms.some((term) => + Object.keys(elementMap).includes(term) + ) + + if (conformToElement) { + let suggestedState = '' + if (conformance == 'mandatory') { + suggestedState = 'enabled' + } + if (conformance == 'notSupported') { + suggestedState = 'disabled' + } + + // generate warning message for required and unsupported elements + element.warningMessage = + `${element.name} has mandatory conformance to ${element.conformance} ` + + `and should be ${suggestedState} when ` + + combinedTerms + + '.' + if (conformance == 'mandatory') { + requiredElements.required[element.id] = element.warningMessage + } + if (conformance == 'notSupported') { + requiredElements.notSupported[element.id] = element.warningMessage + } + } + }) + return requiredElements +} + +/** + * Adds warnings to the session notification table during ZAP file imports + * for features, attributes, commands, and events that do not correctly conform + * within a cluster. + * @param {*} db + * @param {*} endpointId + * @param {*} endpointTypeId + * @param {*} endpointClusterId + * @param {*} deviceTypeRefs + * @param {*} cluster + * @param {*} sessionId + * @returns list of warning messages if any, otherwise false + */ +async function setConformanceWarnings( + db, + endpointId, + endpointTypeId, + endpointClusterId, + deviceTypeRefs, + cluster, + sessionId +) { + let deviceTypeFeatures = await queryFeature.getFeaturesByDeviceTypeRefs( + db, + deviceTypeRefs, + endpointTypeId + ) + let clusterFeatures = deviceTypeFeatures.filter( + (feature) => feature.endpointTypeClusterId == endpointClusterId + ) + + if (clusterFeatures.length > 0) { + let deviceTypeClusterId = clusterFeatures[0].deviceTypeClusterId + let endpointTypeElements = await queryEndpointType.getEndpointTypeElements( + db, + endpointClusterId, + deviceTypeClusterId + ) + + let featureMapVal = clusterFeatures[0].featureMapValue + let featureMap = {} + for (let feature of clusterFeatures) { + let bit = feature.bit + let bitVal = (featureMapVal & (1 << bit)) >> bit + featureMap[feature.code] = bitVal + } + + // get elements that should be mandatory or unsupported based on conformance + let requiredElements = checkElementConformance( + endpointTypeElements, + featureMap + ) + + let warnings = [] + // set warnings for each feature in the cluster + for (const featureData of clusterFeatures) { + let warningInfo = generateWarningMessage( + featureData, + endpointId, + featureMap + ) + if (warningInfo.displayWarning && warningInfo.warningMessage) { + warnings.push(warningInfo.warningMessage) + } + } + + let contextMessage = `⚠Check Feature Compliance on endpoint: ${endpointId}, cluster: ${cluster.name}, ` + + /* If unsupported elements are enabled or required elements are disabled, + they are considered non-conforming. A corresponding warning message will be + generated and added to the warnings array. */ + const filterNonConformElements = ( + elementType, + requiredMap, + notSupportedMap, + elements + ) => { + let elementMap = {} + elements.forEach((element) => { + elementType == 'command' + ? (elementMap[element.id] = element.isEnabled) + : (elementMap[element.id] = element.included) + }) + Object.entries(requiredMap).forEach(([id, message]) => { + if (!(id in elementMap) || !elementMap[id]) { + warnings.push(contextMessage + elementType + ': ' + message) + } + }) + Object.entries(notSupportedMap).forEach(([id, message]) => { + if (id in elementMap && elementMap[id]) { + warnings.push(contextMessage + elementType + ': ' + message) + } + }) + } + + filterNonConformElements( + 'attribute', + requiredElements.attributesToUpdate.required, + requiredElements.attributesToUpdate.notSupported, + endpointTypeElements.attributes + ) + filterNonConformElements( + 'command', + requiredElements.commandsToUpdate.required, + requiredElements.commandsToUpdate.notSupported, + endpointTypeElements.commands + ) + filterNonConformElements( + 'event', + requiredElements.eventsToUpdate.required, + requiredElements.eventsToUpdate.notSupported, + endpointTypeElements.events + ) + + // set warnings in the session notification table + if (warnings.length > 0) { + for (const warning of warnings) { + await querySessionNotice.setNotification( + db, + 'WARNING', + warning, + sessionId, + 1, + 0 + ) + } + return warnings + } + } + return false +} + +exports.checkElementConformance = checkElementConformance +exports.filterRelatedDescElements = filterRelatedDescElements +exports.getOutdatedElementWarning = getOutdatedElementWarning +exports.setConformanceWarnings = setConformanceWarnings diff --git a/src-electron/validation/conformance-expression-evaluator.js b/src-electron/validation/conformance-expression-evaluator.js new file mode 100644 index 0000000000000000000000000000000000000000..34979c70c65b17a72a34794deb39de2da5177a67 --- /dev/null +++ b/src-electron/validation/conformance-expression-evaluator.js @@ -0,0 +1,136 @@ +/** + * + * Copyright (c) 2025 Silicon Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This module provides utilities for evaluating conformance expressions. + * + * @module Validation API: Evaluate conformance expressions + */ + +/** + * Evaluate the value of a boolean conformance expression that includes terms and operators. + * A term can be an attribute, command, event, feature, or conformance abbreviation. + * Operators include AND (&), OR (|), and NOT (!). + * The '[]' indicates optional conformance if the expression inside true. + * Expression containing comma means otherwise conformance. See spec for details. + * Examples of conformance expression: 'A & (!B | C)', 'A & B, [!C]' + * + * @export + * @param {*} expression + * @param {*} elementMap + * @returns 'mandatory', 'optional', 'provisional', or 'notSupported' + */ +function evaluateConformanceExpression(expression, elementMap) { + /** + * helper function to evaluate a single boolean expression + * @param {*} expr + */ + function evaluateBooleanExpression(expr) { + // Replace terms with their actual values from elementMap + expr = expr.replace(/[A-Za-z][A-Za-z0-9_]*/g, (term) => { + if (elementMap[term]) { + return 'true' + } else { + return 'false' + } + }) + + // Evaluate NOT (!) operators + expr = expr.replace(/!true/g, 'false').replace(/!false/g, 'true') + + // Evaluate AND (&) and OR (|) operators by eval() function + return eval(expr) + } + + /** + * helper function to process parentheses and evaluate inner expressions first + * @param {*} expr + */ + function evaluateWithParentheses(expr) { + while (expr.includes('(')) { + expr = expr.replace(/\([^()]+\)/g, (terms) => + evaluateBooleanExpression(terms.slice(1, -1)) + ) + } + return evaluateBooleanExpression(expr) + } + + // Check ',' for otherwise conformance first. + // Split the expression by ',' and evaluate each part in sequence + let parts = expression.split(',') + // if any term is desc, the conformance is too complex to parse + for (let part of parts) { + let terms = part.match(/[A-Za-z][A-Za-z0-9_]*/g) + if (terms && terms.includes('desc')) { + return 'desc' + } + } + for (let part of parts) { + if (part.includes('[') && part.includes(']')) { + // Extract and evaluate the content inside '[]' + let optionalExpr = part.match(/\[(.*?)\]/)[1] + let optionalResult = evaluateWithParentheses(optionalExpr) + if (optionalResult) { + return 'optional' + } else { + return 'notSupported' + } + } else { + part = part.trim() + if (part == 'M') { + return 'mandatory' + } else if (part == 'O') { + return 'optional' + } else if (part == 'D' || part == 'X') { + return 'notSupported' + } else if (part == 'P') { + return 'provisional' + } else { + // Evaluate the part with parentheses if needed + let result = evaluateWithParentheses(part) + if (result) return 'mandatory' + // if the mandatory part is false, go to the next part + } + } + } + + // If none of the parts are true and no optional part was valid, return 'notSupported' + return 'notSupported' +} + +/** + * Check if any terms in the expression are neither a key in the elementMap nor an abbreviation. + * If so, it means the conformance depends on terms with unknown values and changes are not allowed. + * + * @param {*} expression + * @param {*} elementMap + * @returns all missing terms in an array + */ +function checkMissingTerms(expression, elementMap) { + let terms = expression.match(/[A-Za-z][A-Za-z0-9_]*/g) + let missingTerms = [] + let abbreviations = ['M', 'O', 'P', 'D', 'X'] + for (let term of terms) { + if (!(term in elementMap) && !abbreviations.includes(term)) { + missingTerms.push(term) + } + } + return missingTerms +} + +exports.evaluateConformanceExpression = evaluateConformanceExpression +exports.checkMissingTerms = checkMissingTerms diff --git a/src-electron/validation/conformance-xml-parser.js b/src-electron/validation/conformance-xml-parser.js new file mode 100644 index 0000000000000000000000000000000000000000..b73ab9e7bf735de1adae8fd16ba1a6ae7dde1ea3 --- /dev/null +++ b/src-electron/validation/conformance-xml-parser.js @@ -0,0 +1,158 @@ +/** + * + * Copyright (c) 2025 Silicon Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This module provides utilities for parsing conformance data from XML into expressions. + * + * @module Validation API: Parse conformance data from XML + */ + +/** + * Parses conformance from XML data. + * The conformance could come from features, attributes, commands, or events + * + * Call recursive helper function to parse conformance only if the conformance exists. + * Otherwise, return empty string directly + * + * An example of parsing the conformance of 'User' device type feature: + * + * Input operand from xml data: + * { + * "$": {"code": "USR", "name": "User"}, + * "mandatoryConform": [ + * { "andTerm": [ + * { + * "condition": [{"$": {"name": "Matter"}}], + * "orTerm": [ + * { "feature": [ + * { "$": {"name": "PIN"}}, + * { "$": {"name": "RID"}}, + * { "$": {"name": "FGP"}}, + * { "$": {"name": "FACE"}} + * ] + * } + * ] + * } + * ] + * } + * ] + * } + * + * Output conformance string: + * "Matter & (PIN | RID | FGP | FACE)" + * + * @param {*} operand + * @returns The conformance string + */ +function parseConformanceFromXML(operand) { + let hasConformance = Object.keys(operand).some((key) => + key.includes('Conform') + ) + return hasConformance ? parseConformanceRecursively(operand) : '' +} + +/** + * helper function to parse conformance or an operand in conformance recursively + * + * The baseLevelTerms variable include terms that can not have nested terms. + * When they appear, stop recursing and return the name inside directly + * + * @param {*} operand + * @param {*} depth + * @param {*} parentJoinChar + * @returns The conformance string. + */ +function parseConformanceRecursively(operand, depth = 0, parentJoinChar = '') { + if (depth > 200) { + throw new Error(`Maximum recursion depth exceeded + when parsing conformance: ${JSON.stringify(operand)}`) + } + const baseLevelTerms = ['feature', 'condition', 'attribute', 'command'] + if (operand.mandatoryConform) { + let insideTerm = operand.mandatoryConform[0] + // Recurse further if insideTerm is not empty + if (insideTerm && Object.keys(insideTerm).toString() != '$') { + return parseConformanceRecursively(operand.mandatoryConform[0], depth + 1) + } else { + return 'M' + } + } else if (operand.optionalConform) { + let insideTerm = operand.optionalConform[0] + // check '$' key is not the only key in the object to handle special cases + // e.g. '<optionalConform choice="a" more="true"/>' + if (insideTerm && Object.keys(insideTerm).toString() != '$') { + return `[${parseConformanceRecursively(operand.optionalConform[0], depth + 1)}]` + } else { + return 'O' + } + } else if (operand.otherwiseConform) { + return Object.entries(operand.otherwiseConform[0]) + .map(([key, value]) => + parseConformanceRecursively({ [key]: value }, depth + 1) + ) + .join(', ') + } else if (operand.notTerm) { + // need to surround terms inside a notTerm with '()' if it contains multiple terms + // e.g. !(A | B) or !(A & B) + // able to process multiple parallel notTerms, e.g. !A & !B + return operand.notTerm + .map((term) => { + let nt = parseConformanceRecursively(term, depth + 1) + return nt.includes('&') || nt.includes('|') ? `!(${nt})` : `!${nt}` + }) + .join(` ${parentJoinChar} `) + } else if (operand.andTerm || operand.orTerm) { + // process andTerm and orTerm in the same logic + // when joining multiple orTerms inside andTerms, we need to + // surround them with '()', vice versa for andTerms inside orTerms + // e.g. A & (B | C) or A | (B & C) + let joinChar = operand.andTerm ? '&' : '|' + let termKey = operand.andTerm ? 'andTerm' : 'orTerm' + let oppositeChar = joinChar == '&' ? '|' : '&' + return Object.entries(operand[termKey][0]) + .map(([key, value]) => { + if (baseLevelTerms.includes(key)) { + return value.map((operand) => operand.$.name).join(` ${joinChar} `) + } else { + let terms = parseConformanceRecursively( + { [key]: value }, + depth + 1, + joinChar + ) + return terms.includes(oppositeChar) ? `(${terms})` : terms + } + }) + .join(` ${joinChar} `) + } else if (operand.provisionalConform) { + return 'P' + } else if (operand.disallowConform) { + return 'X' + } else if (operand.deprecateConform) { + return 'D' + } else { + // reach base level terms, return the name directly + for (const term of baseLevelTerms) { + if (operand[term]) { + return operand[term][0].$.name + } + } + // reaching here means the term is too complex to parse + return 'desc' + } +} + +exports.parseConformanceFromXML = parseConformanceFromXML diff --git a/src-electron/zcl/zcl-loader-silabs.js b/src-electron/zcl/zcl-loader-silabs.js index df854b2ed35cb7e53264bf27934cadb5ce05c1ca..569d45cbd841a6f1c0c80deb38406dab129a15c4 100644 --- a/src-electron/zcl/zcl-loader-silabs.js +++ b/src-electron/zcl/zcl-loader-silabs.js @@ -40,6 +40,7 @@ const _ = require('lodash') const querySessionNotification = require('../db/query-session-notification') const queryPackageNotification = require('../db/query-package-notification') const newDataModel = require('./zcl-loader-new-data-model') +const conformParser = require('../validation/conformance-xml-parser') /** * Promises to read the JSON file and resolve all the data. @@ -492,7 +493,7 @@ function prepareCluster(cluster, context, isExtension = false) { description: command.description ? command.description[0].trim() : '', source: command.$.source, isOptional: command.$.optional == 'true' ? true : false, - conformance: parseConformanceFromXML(command), + conformance: conformParser.parseConformanceFromXML(command), mustUseTimedInvoke: command.$.mustUseTimedInvoke == 'true', introducedIn: command.$.introducedIn, removedIn: command.$.removedIn, @@ -548,7 +549,7 @@ function prepareCluster(cluster, context, isExtension = false) { manufacturerCode: event.$.manufacturerCode, name: event.$.name, side: event.$.side, - conformance: parseConformanceFromXML(event), + conformance: conformParser.parseConformanceFromXML(event), priority: event.$.priority, description: event.description ? event.description[0].trim() : '', isOptional: event.$.optional == 'true', @@ -649,7 +650,7 @@ function prepareCluster(cluster, context, isExtension = false) { : attribute.$.type, side: attribute.$.side, define: attribute.$.define, - conformance: parseConformanceFromXML(attribute), + conformance: conformParser.parseConformanceFromXML(attribute), min: attribute.$.min, max: attribute.$.max, minLength: 0, @@ -737,7 +738,7 @@ function prepareCluster(cluster, context, isExtension = false) { bit: feature.$.bit, defaultValue: feature.$.default, description: feature.$.summary, - conformance: parseConformanceFromXML(feature) + conformance: conformParser.parseConformanceFromXML(feature) } ret.features.push(f) @@ -1693,7 +1694,7 @@ function prepareDeviceType(deviceType) { include.features[0].feature.forEach((f) => { features.push({ code: f.$.code, - conformance: parseConformanceFromXML(f) + conformance: conformParser.parseConformanceFromXML(f) }) }) } @@ -2393,140 +2394,6 @@ async function parseFeatureFlags(db, packageId, featureFlags) { ) } -/** - * Parses conformance from XML data. - * The conformance could come from features, attributes, commands, or events - * - * Call recursive helper function to parse conformance only if the conformance exists. - * Otherwise, return empty string directly - * - * An example of parsing the conformance of 'User' device type feature: - * - * Input operand from xml data: - * { - * "$": {"code": "USR", "name": "User"}, - * "mandatoryConform": [ - * { "andTerm": [ - * { - * "condition": [{"$": {"name": "Matter"}}], - * "orTerm": [ - * { "feature": [ - * { "$": {"name": "PIN"}}, - * { "$": {"name": "RID"}}, - * { "$": {"name": "FGP"}}, - * { "$": {"name": "FACE"}} - * ] - * } - * ] - * } - * ] - * } - * ] - * } - * - * Output conformance string: - * "Matter & (PIN | RID | FGP | FACE)" - * - * @param {*} operand - * @returns The conformance string - */ -function parseConformanceFromXML(operand) { - let hasConformance = Object.keys(operand).some((key) => - key.includes('Conform') - ) - return hasConformance ? parseConformanceRecursively(operand) : '' -} - -/** - * helper function to parse conformance or an operand in conformance recursively - * - * The baseLevelTerms variable include terms that can not have nested terms. - * When they appear, stop recursing and return the name inside directly - * - * @param {*} operand - * @param {*} depth - * @param {*} parentJoinChar - * @returns The conformance string. - */ -function parseConformanceRecursively(operand, depth = 0, parentJoinChar = '') { - if (depth > 200) { - throw new Error(`Maximum recursion depth exceeded - when parsing conformance: ${JSON.stringify(operand)}`) - } - const baseLevelTerms = ['feature', 'condition', 'attribute', 'command'] - if (operand.mandatoryConform) { - let insideTerm = operand.mandatoryConform[0] - // Recurse further if insideTerm is not empty - if (insideTerm && Object.keys(insideTerm).toString() != '$') { - return parseConformanceRecursively(operand.mandatoryConform[0], depth + 1) - } else { - return 'M' - } - } else if (operand.optionalConform) { - let insideTerm = operand.optionalConform[0] - // check '$' key is not the only key in the object to handle special cases - // e.g. '<optionalConform choice="a" more="true"/>' - if (insideTerm && Object.keys(insideTerm).toString() != '$') { - return `[${parseConformanceRecursively(operand.optionalConform[0], depth + 1)}]` - } else { - return 'O' - } - } else if (operand.otherwiseConform) { - return Object.entries(operand.otherwiseConform[0]) - .map(([key, value]) => - parseConformanceRecursively({ [key]: value }, depth + 1) - ) - .join(', ') - } else if (operand.notTerm) { - // need to surround terms inside a notTerm with '()' if it contains multiple terms - // e.g. !(A | B) or !(A & B) - // able to process multiple parallel notTerms, e.g. !A & !B - return operand.notTerm - .map((term) => { - let nt = parseConformanceRecursively(term, depth + 1) - return nt.includes('&') || nt.includes('|') ? `!(${nt})` : `!${nt}` - }) - .join(` ${parentJoinChar} `) - } else if (operand.andTerm || operand.orTerm) { - // process andTerm and orTerm in the same logic - // when joining multiple orTerms inside andTerms, we need to - // surround them with '()', vice versa for andTerms inside orTerms - // e.g. A & (B | C) or A | (B & C) - let joinChar = operand.andTerm ? '&' : '|' - let termKey = operand.andTerm ? 'andTerm' : 'orTerm' - let oppositeChar = joinChar == '&' ? '|' : '&' - return Object.entries(operand[termKey][0]) - .map(([key, value]) => { - if (baseLevelTerms.includes(key)) { - return value.map((operand) => operand.$.name).join(` ${joinChar} `) - } else { - let terms = parseConformanceRecursively( - { [key]: value }, - depth + 1, - joinChar - ) - return terms.includes(oppositeChar) ? `(${terms})` : terms - } - }) - .join(` ${joinChar} `) - } else if (operand.provisionalConform) { - return 'P' - } else if (operand.disallowConform) { - return 'X' - } else if (operand.deprecateConform) { - return 'D' - } else { - // reach base level terms, return the name directly - for (const term of baseLevelTerms) { - if (operand[term]) { - return operand[term][0].$.name - } - } - // reaching here means the term is too complex to parse - return 'desc' - } -} - /** * Inside the `zcl.json` can be a `featureFlags` key, which is * a general purpose object. It contains keys, that map to objects. diff --git a/test/feature.test.js b/test/feature.test.js index 87ea3413d59aadea0e9cf0c37810e80cbd5a6934..dcaa06b4ed7d865a381719d8156b2df41fe4a1d7 100644 --- a/test/feature.test.js +++ b/test/feature.test.js @@ -26,6 +26,8 @@ const queryFeature = require('../src-electron/db/query-feature') const querySession = require('../src-electron/db/query-session') const queryConfig = require('../src-electron/db/query-config') const util = require('../src-electron/util/util') +const conformEvaluator = require('../src-electron/validation/conformance-expression-evaluator') +const conformChecker = require('../src-electron/validation/conformance-checker') let db let ctx @@ -216,7 +218,7 @@ test( ] conformanceExpressions.forEach((expression) => { - let result = queryFeature.evaluateConformanceExpression( + let result = conformEvaluator.evaluateConformanceExpression( expression.expression, elementMap ) @@ -239,8 +241,8 @@ test( { name: 'Element7', conformance: 'description' } ] - let result = queryFeature.filterElementsContainingDesc(elements) - expect(result).toEqual([ + let descTerms = conformChecker.filterRelatedDescElements(elements, 'desc') + expect(descTerms).toEqual([ { name: 'Element1', conformance: 'desc' }, { name: 'Element2', conformance: 'P, desc' }, { name: 'Element3', conformance: '[desc & XY]' }, @@ -249,8 +251,13 @@ test( ]) const featureCode = 'HS' - result = queryFeature.filterRelatedDescElements(elements, featureCode) - expect(result).toEqual([{ name: 'Element4', conformance: 'desc, [HS]' }]) + let relatedDescTerms = conformChecker.filterRelatedDescElements( + elements, + featureCode + ) + expect(relatedDescTerms).toEqual([ + { name: 'Element4', conformance: 'desc, [HS]' } + ]) }, testUtil.timeout.short() ) @@ -330,7 +337,7 @@ test( // 1. test enable an optional feature featureMap['HS'] = 1 - result = queryFeature.checkElementConformance( + result = conformChecker.checkElementConformance( elements, featureMap, featureHS, @@ -355,7 +362,7 @@ test( // 2. test disable a mandatory feature featureMap['XY'] = 0 - result = queryFeature.checkElementConformance( + result = conformChecker.checkElementConformance( elements, featureMap, featureXY, @@ -382,7 +389,7 @@ test( // 3. test enable a feature with unknown conformance featureMap['UNKNOWN'] = 1 - result = queryFeature.checkElementConformance( + result = conformChecker.checkElementConformance( elements, featureMap, featureUnknown, @@ -412,7 +419,7 @@ test( } elements.attributes.push(descElement) featureMap['HS'] = 1 - result = queryFeature.checkElementConformance( + result = conformChecker.checkElementConformance( elements, featureMap, featureHS, @@ -436,7 +443,7 @@ test( elements.attributes.find((attr) => attr.name == 'DescElement').conformance = 'desc, [UNKNOWN]' featureMap['UNKNOWN'] = 1 - result = queryFeature.checkElementConformance( + result = conformChecker.checkElementConformance( elements, featureMap, featureUnknown,