Allow keeping default values for Attribute Access Interface attributes. (#1726)
Attributes named in attributeAccessInterfaceAttributes are forced to
External storage, which normally drops their default so nothing is
generated for a fixed value such as FeatureMap.
An entry can now opt in with keepDefault:
"Software Diagnostics": [
"CurrentHeapFree",
{ "name": "FeatureMap", "keepDefault": true }
]
The attribute stays External and takes no attribute-store space, but the
default remains editable and is generated into the endpoint config.
Plain string entries keep their previous meaning.diff --git a/docs/api.md b/docs/api.md
index 4d61e0a..4198206 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -10270,6 +10270,9 @@
* [~endpoint_attribute_long_defaults(options)](#module_Templating API_ Matter endpoint config helpers..endpoint_attribute_long_defaults) ⇒
* [~asMEI(manufacturerCode, code)](#module_Templating API_ Matter endpoint config helpers..asMEI) ⇒
* [~determineAttributeDefaultValue(specifiedDefault, type, typeSize, isNullable, db, sessionId)](#module_Templating API_ Matter endpoint config helpers..determineAttributeDefaultValue) ⇒
+ * [~keepDefaultValueKey(clusterName, attributeName)](#module_Templating API_ Matter endpoint config helpers..keepDefaultValueKey) ⇒
+ * [~collectAttributesKeepingDefaultValue(db, zclPackageIds)](#module_Templating API_ Matter endpoint config helpers..collectAttributesKeepingDefaultValue) ⇒
+ * [~keepsDefaultValueForExternalStorage(attribute, cluster, options)](#module_Templating API_ Matter endpoint config helpers..keepsDefaultValueForExternalStorage) ⇒
* [~collectAttributes()](#module_Templating API_ Matter endpoint config helpers..collectAttributes)
* [~collectAttributeSizes(db, zclPackageIds, endpointTypes)](#module_Templating API_ Matter endpoint config helpers..collectAttributeSizes) ⇒
* [~collectAttributeTypeInfo(db, zclPackageIds, endpointTypes)](#module_Templating API_ Matter endpoint config helpers..collectAttributeTypeInfo) ⇒
@@ -10725,6 +10728,55 @@
| db | <code>\*</code> |
| sessionId | <code>\*</code> |
+<a name="module_Templating API_ Matter endpoint config helpers..keepDefaultValueKey"></a>
+
+### Templating API: Matter endpoint config helpers~keepDefaultValueKey(clusterName, attributeName) ⇒
+Builds the key under which a cluster/attribute pair is looked up in the set
+of attributes that keep their default value.
+
+**Kind**: inner method of [<code>Templating API: Matter endpoint config helpers</code>](#module_Templating API_ Matter endpoint config helpers)
+**Returns**: the lookup key
+
+| Param | Type |
+| --- | --- |
+| clusterName | <code>\*</code> |
+| attributeName | <code>\*</code> |
+
+<a name="module_Templating API_ Matter endpoint config helpers..collectAttributesKeepingDefaultValue"></a>
+
+### Templating API: Matter endpoint config helpers~collectAttributesKeepingDefaultValue(db, zclPackageIds) ⇒
+Collects the cluster/attribute pairs whose storage policy forces external
+storage but keeps the default value under ZAP control.
+
+The storage policy of a global attribute, such as FeatureMap, is not on the
+attribute itself: there is one attribute row shared by every cluster, so the
+policy is recorded per cluster/attribute pair instead. That is why this is
+looked up by name rather than read off the attribute.
+
+**Kind**: inner method of [<code>Templating API: Matter endpoint config helpers</code>](#module_Templating API_ Matter endpoint config helpers)
+**Returns**: a set of keys built by keepDefaultValueKey
+
+| Param | Type |
+| --- | --- |
+| db | <code>\*</code> |
+| zclPackageIds | <code>\*</code> |
+
+<a name="module_Templating API_ Matter endpoint config helpers..keepsDefaultValueForExternalStorage"></a>
+
+### Templating API: Matter endpoint config helpers~keepsDefaultValueForExternalStorage(attribute, cluster, options) ⇒
+Tells whether an external attribute keeps its default value, which means that
+the default value is generated even though the attribute takes up no space in
+the attribute store.
+
+**Kind**: inner method of [<code>Templating API: Matter endpoint config helpers</code>](#module_Templating API_ Matter endpoint config helpers)
+**Returns**: true if the default value is kept
+
+| Param | Type | Description |
+| --- | --- | --- |
+| attribute | <code>\*</code> | |
+| cluster | <code>\*</code> | |
+| options | <code>\*</code> | collectAttributes options |
+
<a name="module_Templating API_ Matter endpoint config helpers..collectAttributes"></a>
### Templating API: Matter endpoint config helpers~collectAttributes()
@@ -22790,6 +22842,7 @@
* [~prepXmlCommand(c)](#module_Loader API_ Loader APIs..prepXmlCommand) ⇒
* [~prepXmlEvent(e)](#module_Loader API_ Loader APIs..prepXmlEvent) ⇒
* [~parseSingleNewXmlFile(f)](#module_Loader API_ Loader APIs..parseSingleNewXmlFile) ⇒
+ * [~normalizeAttributeAccessInterfaceAttributes(attributeAccessInterfaceAttributes)](#module_Loader API_ Loader APIs..normalizeAttributeAccessInterfaceAttributes) ⇒
* [~collectDataFromJsonFile(ctx)](#module_Loader API_ Loader APIs..collectDataFromJsonFile) ⇒
* [~collectDataFromPropertiesFile(ctx)](#module_Loader API_ Loader APIs..collectDataFromPropertiesFile) ⇒
* [~maskToType(mask)](#module_Loader API_ Loader APIs..maskToType) ⇒
@@ -23514,6 +23567,34 @@
| --- | --- |
| f | <code>\*</code> |
+<a name="module_Loader API_ Loader APIs..normalizeAttributeAccessInterfaceAttributes"></a>
+
+### Loader API: Loader APIs~normalizeAttributeAccessInterfaceAttributes(attributeAccessInterfaceAttributes) ⇒
+Normalizes the attributeAccessInterfaceAttributes section of the metadata
+file into a list of { name, keepDefault } objects per cluster.
+
+An entry is either the name of an attribute, which means that the attribute
+is handled entirely by the attribute access interface:
+
+ "Access Control": [ "SubjectsPerAccessControlEntry" ]
+
+or an object that names the attribute and asks for its default value to stay
+under ZAP control:
+
+ "Access Control": [ { "name": "FeatureMap", "keepDefault": true } ]
+
+Both are forced to external storage. The second form does not change that:
+it only leaves the default value editable and generated, so the
+implementation can read it out of the generated endpoint configuration
+instead of holding it itself.
+
+**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs)
+**Returns**: the same clusters, with every entry turned into an object
+
+| Param | Type | Description |
+| --- | --- | --- |
+| attributeAccessInterfaceAttributes | <code>\*</code> | contents of the metadata section |
+
<a name="module_Loader API_ Loader APIs..collectDataFromJsonFile"></a>
### Loader API: Loader APIs~collectDataFromJsonFile(ctx) ⇒
@@ -24366,7 +24447,7 @@
| --- | --- | --- |
| db | <code>\*</code> | The database connection object. |
| pkgRef | <code>\*</code> | The package reference id for which the attributes are being parsed. |
-| attributeAccessInterfaceAttributes | <code>\*</code> | An object containing the attribute access interface attributes, structured by cluster. |
+| attributeAccessInterfaceAttributes | <code>\*</code> | An object containing the attribute access interface attributes, structured by cluster, as returned by normalizeAttributeAccessInterfaceAttributes. |
<a name="module_Loader API_ Loader APIs..parseDefaults"></a>
@@ -24667,6 +24748,7 @@
* [~prepXmlCommand(c)](#module_Loader API_ Loader APIs..prepXmlCommand) ⇒
* [~prepXmlEvent(e)](#module_Loader API_ Loader APIs..prepXmlEvent) ⇒
* [~parseSingleNewXmlFile(f)](#module_Loader API_ Loader APIs..parseSingleNewXmlFile) ⇒
+ * [~normalizeAttributeAccessInterfaceAttributes(attributeAccessInterfaceAttributes)](#module_Loader API_ Loader APIs..normalizeAttributeAccessInterfaceAttributes) ⇒
* [~collectDataFromJsonFile(ctx)](#module_Loader API_ Loader APIs..collectDataFromJsonFile) ⇒
* [~collectDataFromPropertiesFile(ctx)](#module_Loader API_ Loader APIs..collectDataFromPropertiesFile) ⇒
* [~maskToType(mask)](#module_Loader API_ Loader APIs..maskToType) ⇒
@@ -25391,6 +25473,34 @@
| --- | --- |
| f | <code>\*</code> |
+<a name="module_Loader API_ Loader APIs..normalizeAttributeAccessInterfaceAttributes"></a>
+
+### Loader API: Loader APIs~normalizeAttributeAccessInterfaceAttributes(attributeAccessInterfaceAttributes) ⇒
+Normalizes the attributeAccessInterfaceAttributes section of the metadata
+file into a list of { name, keepDefault } objects per cluster.
+
+An entry is either the name of an attribute, which means that the attribute
+is handled entirely by the attribute access interface:
+
+ "Access Control": [ "SubjectsPerAccessControlEntry" ]
+
+or an object that names the attribute and asks for its default value to stay
+under ZAP control:
+
+ "Access Control": [ { "name": "FeatureMap", "keepDefault": true } ]
+
+Both are forced to external storage. The second form does not change that:
+it only leaves the default value editable and generated, so the
+implementation can read it out of the generated endpoint configuration
+instead of holding it itself.
+
+**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs)
+**Returns**: the same clusters, with every entry turned into an object
+
+| Param | Type | Description |
+| --- | --- | --- |
+| attributeAccessInterfaceAttributes | <code>\*</code> | contents of the metadata section |
+
<a name="module_Loader API_ Loader APIs..collectDataFromJsonFile"></a>
### Loader API: Loader APIs~collectDataFromJsonFile(ctx) ⇒
@@ -26243,7 +26353,7 @@
| --- | --- | --- |
| db | <code>\*</code> | The database connection object. |
| pkgRef | <code>\*</code> | The package reference id for which the attributes are being parsed. |
-| attributeAccessInterfaceAttributes | <code>\*</code> | An object containing the attribute access interface attributes, structured by cluster. |
+| attributeAccessInterfaceAttributes | <code>\*</code> | An object containing the attribute access interface attributes, structured by cluster, as returned by normalizeAttributeAccessInterfaceAttributes. |
<a name="module_Loader API_ Loader APIs..parseDefaults"></a>
@@ -26544,6 +26654,7 @@
* [~prepXmlCommand(c)](#module_Loader API_ Loader APIs..prepXmlCommand) ⇒
* [~prepXmlEvent(e)](#module_Loader API_ Loader APIs..prepXmlEvent) ⇒
* [~parseSingleNewXmlFile(f)](#module_Loader API_ Loader APIs..parseSingleNewXmlFile) ⇒
+ * [~normalizeAttributeAccessInterfaceAttributes(attributeAccessInterfaceAttributes)](#module_Loader API_ Loader APIs..normalizeAttributeAccessInterfaceAttributes) ⇒
* [~collectDataFromJsonFile(ctx)](#module_Loader API_ Loader APIs..collectDataFromJsonFile) ⇒
* [~collectDataFromPropertiesFile(ctx)](#module_Loader API_ Loader APIs..collectDataFromPropertiesFile) ⇒
* [~maskToType(mask)](#module_Loader API_ Loader APIs..maskToType) ⇒
@@ -27268,6 +27379,34 @@
| --- | --- |
| f | <code>\*</code> |
+<a name="module_Loader API_ Loader APIs..normalizeAttributeAccessInterfaceAttributes"></a>
+
+### Loader API: Loader APIs~normalizeAttributeAccessInterfaceAttributes(attributeAccessInterfaceAttributes) ⇒
+Normalizes the attributeAccessInterfaceAttributes section of the metadata
+file into a list of { name, keepDefault } objects per cluster.
+
+An entry is either the name of an attribute, which means that the attribute
+is handled entirely by the attribute access interface:
+
+ "Access Control": [ "SubjectsPerAccessControlEntry" ]
+
+or an object that names the attribute and asks for its default value to stay
+under ZAP control:
+
+ "Access Control": [ { "name": "FeatureMap", "keepDefault": true } ]
+
+Both are forced to external storage. The second form does not change that:
+it only leaves the default value editable and generated, so the
+implementation can read it out of the generated endpoint configuration
+instead of holding it itself.
+
+**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs)
+**Returns**: the same clusters, with every entry turned into an object
+
+| Param | Type | Description |
+| --- | --- | --- |
+| attributeAccessInterfaceAttributes | <code>\*</code> | contents of the metadata section |
+
<a name="module_Loader API_ Loader APIs..collectDataFromJsonFile"></a>
### Loader API: Loader APIs~collectDataFromJsonFile(ctx) ⇒
@@ -28120,7 +28259,7 @@
| --- | --- | --- |
| db | <code>\*</code> | The database connection object. |
| pkgRef | <code>\*</code> | The package reference id for which the attributes are being parsed. |
-| attributeAccessInterfaceAttributes | <code>\*</code> | An object containing the attribute access interface attributes, structured by cluster. |
+| attributeAccessInterfaceAttributes | <code>\*</code> | An object containing the attribute access interface attributes, structured by cluster, as returned by normalizeAttributeAccessInterfaceAttributes. |
<a name="module_Loader API_ Loader APIs..parseDefaults"></a>
@@ -28421,6 +28560,7 @@
* [~prepXmlCommand(c)](#module_Loader API_ Loader APIs..prepXmlCommand) ⇒
* [~prepXmlEvent(e)](#module_Loader API_ Loader APIs..prepXmlEvent) ⇒
* [~parseSingleNewXmlFile(f)](#module_Loader API_ Loader APIs..parseSingleNewXmlFile) ⇒
+ * [~normalizeAttributeAccessInterfaceAttributes(attributeAccessInterfaceAttributes)](#module_Loader API_ Loader APIs..normalizeAttributeAccessInterfaceAttributes) ⇒
* [~collectDataFromJsonFile(ctx)](#module_Loader API_ Loader APIs..collectDataFromJsonFile) ⇒
* [~collectDataFromPropertiesFile(ctx)](#module_Loader API_ Loader APIs..collectDataFromPropertiesFile) ⇒
* [~maskToType(mask)](#module_Loader API_ Loader APIs..maskToType) ⇒
@@ -29145,6 +29285,34 @@
| --- | --- |
| f | <code>\*</code> |
+<a name="module_Loader API_ Loader APIs..normalizeAttributeAccessInterfaceAttributes"></a>
+
+### Loader API: Loader APIs~normalizeAttributeAccessInterfaceAttributes(attributeAccessInterfaceAttributes) ⇒
+Normalizes the attributeAccessInterfaceAttributes section of the metadata
+file into a list of { name, keepDefault } objects per cluster.
+
+An entry is either the name of an attribute, which means that the attribute
+is handled entirely by the attribute access interface:
+
+ "Access Control": [ "SubjectsPerAccessControlEntry" ]
+
+or an object that names the attribute and asks for its default value to stay
+under ZAP control:
+
+ "Access Control": [ { "name": "FeatureMap", "keepDefault": true } ]
+
+Both are forced to external storage. The second form does not change that:
+it only leaves the default value editable and generated, so the
+implementation can read it out of the generated endpoint configuration
+instead of holding it itself.
+
+**Kind**: inner method of [<code>Loader API: Loader APIs</code>](#module_Loader API_ Loader APIs)
+**Returns**: the same clusters, with every entry turned into an object
+
+| Param | Type | Description |
+| --- | --- | --- |
+| attributeAccessInterfaceAttributes | <code>\*</code> | contents of the metadata section |
+
<a name="module_Loader API_ Loader APIs..collectDataFromJsonFile"></a>
### Loader API: Loader APIs~collectDataFromJsonFile(ctx) ⇒
@@ -29997,7 +30165,7 @@
| --- | --- | --- |
| db | <code>\*</code> | The database connection object. |
| pkgRef | <code>\*</code> | The package reference id for which the attributes are being parsed. |
-| attributeAccessInterfaceAttributes | <code>\*</code> | An object containing the attribute access interface attributes, structured by cluster. |
+| attributeAccessInterfaceAttributes | <code>\*</code> | An object containing the attribute access interface attributes, structured by cluster, as returned by normalizeAttributeAccessInterfaceAttributes. |
<a name="module_Loader API_ Loader APIs..parseDefaults"></a>
diff --git a/docs/helpers.md b/docs/helpers.md
index 7a1aad7..4cc4217 100644
--- a/docs/helpers.md
+++ b/docs/helpers.md
@@ -1106,6 +1106,9 @@
* [~endpoint_attribute_long_defaults(options)](#module_Templating API_ Matter endpoint config helpers..endpoint_attribute_long_defaults) ⇒
* [~asMEI(manufacturerCode, code)](#module_Templating API_ Matter endpoint config helpers..asMEI) ⇒
* [~determineAttributeDefaultValue(specifiedDefault, type, typeSize, isNullable, db, sessionId)](#module_Templating API_ Matter endpoint config helpers..determineAttributeDefaultValue) ⇒
+ * [~keepDefaultValueKey(clusterName, attributeName)](#module_Templating API_ Matter endpoint config helpers..keepDefaultValueKey) ⇒
+ * [~collectAttributesKeepingDefaultValue(db, zclPackageIds)](#module_Templating API_ Matter endpoint config helpers..collectAttributesKeepingDefaultValue) ⇒
+ * [~keepsDefaultValueForExternalStorage(attribute, cluster, options)](#module_Templating API_ Matter endpoint config helpers..keepsDefaultValueForExternalStorage) ⇒
* [~collectAttributes()](#module_Templating API_ Matter endpoint config helpers..collectAttributes)
* [~collectAttributeSizes(db, zclPackageIds, endpointTypes)](#module_Templating API_ Matter endpoint config helpers..collectAttributeSizes) ⇒
* [~collectAttributeTypeInfo(db, zclPackageIds, endpointTypes)](#module_Templating API_ Matter endpoint config helpers..collectAttributeTypeInfo) ⇒
@@ -1561,6 +1564,55 @@
| db | <code>\*</code> |
| sessionId | <code>\*</code> |
+<a name="module_Templating API_ Matter endpoint config helpers..keepDefaultValueKey"></a>
+
+### Templating API: Matter endpoint config helpers~keepDefaultValueKey(clusterName, attributeName) ⇒
+Builds the key under which a cluster/attribute pair is looked up in the set
+of attributes that keep their default value.
+
+**Kind**: inner method of [<code>Templating API: Matter endpoint config helpers</code>](#module_Templating API_ Matter endpoint config helpers)
+**Returns**: the lookup key
+
+| Param | Type |
+| --- | --- |
+| clusterName | <code>\*</code> |
+| attributeName | <code>\*</code> |
+
+<a name="module_Templating API_ Matter endpoint config helpers..collectAttributesKeepingDefaultValue"></a>
+
+### Templating API: Matter endpoint config helpers~collectAttributesKeepingDefaultValue(db, zclPackageIds) ⇒
+Collects the cluster/attribute pairs whose storage policy forces external
+storage but keeps the default value under ZAP control.
+
+The storage policy of a global attribute, such as FeatureMap, is not on the
+attribute itself: there is one attribute row shared by every cluster, so the
+policy is recorded per cluster/attribute pair instead. That is why this is
+looked up by name rather than read off the attribute.
+
+**Kind**: inner method of [<code>Templating API: Matter endpoint config helpers</code>](#module_Templating API_ Matter endpoint config helpers)
+**Returns**: a set of keys built by keepDefaultValueKey
+
+| Param | Type |
+| --- | --- |
+| db | <code>\*</code> |
+| zclPackageIds | <code>\*</code> |
+
+<a name="module_Templating API_ Matter endpoint config helpers..keepsDefaultValueForExternalStorage"></a>
+
+### Templating API: Matter endpoint config helpers~keepsDefaultValueForExternalStorage(attribute, cluster, options) ⇒
+Tells whether an external attribute keeps its default value, which means that
+the default value is generated even though the attribute takes up no space in
+the attribute store.
+
+**Kind**: inner method of [<code>Templating API: Matter endpoint config helpers</code>](#module_Templating API_ Matter endpoint config helpers)
+**Returns**: true if the default value is kept
+
+| Param | Type | Description |
+| --- | --- | --- |
+| attribute | <code>\*</code> | |
+| cluster | <code>\*</code> | |
+| options | <code>\*</code> | collectAttributes options |
+
<a name="module_Templating API_ Matter endpoint config helpers..collectAttributes"></a>
### Templating API: Matter endpoint config helpers~collectAttributes()
diff --git a/docs/sdk-integration.md b/docs/sdk-integration.md
index ec1b786..d595153 100644
--- a/docs/sdk-integration.md
+++ b/docs/sdk-integration.md
@@ -47,6 +47,56 @@
If the aggregation metafile is of a type `zcl.json` or `zcl.properties`, then the XML files it refers to, are assumed to be of the Silicon Labs XML format.
If the aggregation metafile is using `library.xml` type, then the XML files provided by it are assumed to be of the Zigbee XML format.
+### Attributes handled by the attribute access interface
+
+Some attributes are not held in the attribute store at all: the implementation
+answers reads and writes for them itself. A `zcl.json` metafile names those
+attributes in `attributeAccessInterfaceAttributes`, per cluster. ZAP forces them
+to `External` storage and does not let that be changed in the UI:
+
+```json
+"attributeAccessInterfaceAttributes": {
+ "Access Control": [
+ "SubjectsPerAccessControlEntry",
+ "TargetsPerAccessControlEntry",
+ "AccessControlEntriesPerFabric"
+ ]
+}
+```
+
+By default such an attribute has no default value: the field is not editable in
+the UI and nothing is generated for it, since there is no attribute store slot
+for a default value to be copied into.
+
+That is not what every implementation wants. An attribute such as `FeatureMap`
+holds a fixed value that is decided when the endpoint is configured, and an
+implementation that answers reads for it still has to get that value from
+somewhere. Holding it in the implementation means holding it in RAM, for a value
+that never changes. An entry can therefore ask for the default value to stay
+under ZAP control:
+
+```json
+"attributeAccessInterfaceAttributes": {
+ "Software Diagnostics": [
+ "CurrentHeapFree",
+ { "name": "FeatureMap", "keepDefault": true }
+ ]
+}
+```
+
+Both forms can be mixed in the same list. For an entry with `keepDefault`:
+
+- the attribute is still forced to `External` storage, so it takes up no space
+ in the attribute store;
+- its default value stays editable in the UI;
+- its default value is generated into the endpoint configuration, so the
+ implementation can read it out of the generated (read only) data instead of
+ holding a copy of it.
+
+A default value can only be kept when its size is known, which rules out list
+typed attributes and attributes whose type has no fixed size. Asking to keep the
+default value of one of those has no effect.
+
## Generation templates and extensions
Generation templates and extensions are provided by the SDK. They are the input to the ZAP tool. They control generation and tailor the ZAP tool specifically to a given SDK, by providing the correct details of the implementation that ZAP cares about.
@@ -79,7 +129,7 @@
| name | string | Human readable name of the template. Mostly used in logging and menu items. |
| output | string | Name of the output file generated. May contain replacement patterns (see below). |
| iterator | string | If this template produces multiple files, iterating over certain object, then this field will be present. Possible values are `availableCluster`, `selectedCluster`,`selectedClientCluster`, `selectedServerCluster`. |
-| static | string | If set to the string 'true', marks this template as a static template. Static templates will not generate if `generateStaticTemplates` is false for the session. |
+| static | string | If set to the string 'true', marks this template as a static template. Static templates will not generate if `generateStaticTemplates` is false for the session. |
The _replacement pattern_ inside the output key, comes handy when iterator key is used and defines how each generated file will be named. Replacement patterns are in a format of `{key}` or `{key:modifier}`. The `key` can be any usual key that the iterated object provides. For example, if you iterate over cluster, these can be `code`, `name`, `description`, `define` and all the usual keys that a cluster supports. So if your output contains `{code}` then this pattern will be replaced by the actual cluster code.
@@ -101,12 +151,12 @@
Following table lists generator options supported by the template generator category.
| Key | Value |
-| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| postProcessMulti | a string, representing command to execute, with the list of generated files appended in a single command line. This would be typically used by zap to execute commands like `uncrustify`, or similar post-processing, which can take multiple file names as arguments. |
| postProcessSingle | a string, representing a command to execute for each individual file appended in a single command line. This would be typically used by commands or scripts that clean up the generated files, and can only take one file name at a time as an argument. |
| postProcessConditionalFile | a path, representing a file. If this file does not exist, then a specified post-process action will not get executed. |
| routeErrToOut | a boolean flag. If it's set to true, then any stderr from the post-process will be routed to stdout. This is due that a driving process sometimes has trouble dealing with both stdout and stderr, so this flag might help you preserve the logs. |
-| generateStaticTemplates | string | A string flag ('true'/'false'). The default for whether static templates are generated. If 'false', they are not. A session-key in the .zap file can override this default. |
+| generateStaticTemplates | string | A string flag ('true'/'false'). The default for whether static templates are generated. If 'false', they are not. A session-key in the .zap file can override this default. |
## Template key: override
diff --git a/src-electron/db/query-config.js b/src-electron/db/query-config.js
index 817f943..0835343 100644
--- a/src-electron/db/query-config.js
+++ b/src-electron/db/query-config.js
@@ -257,7 +257,16 @@
staticAttribute.storagePolicy ==
dbEnum.storagePolicy.attributeAccessInterface
) {
- staticAttribute.defaultValue = null
+ let cluster = await queryZcl.selectClusterById(db, clusterRef)
+ if (
+ !queryUpgrade.keepsDefault(
+ forcedExternal,
+ cluster ? cluster.name : null,
+ staticAttribute.name
+ )
+ ) {
+ staticAttribute.defaultValue = null
+ }
}
if (staticAttribute == null) {
throw new Error(`COULD NOT LOCATE ATTRIBUTE: ${attributeId} `)
diff --git a/src-electron/db/query-endpoint.js b/src-electron/db/query-endpoint.js
index bd4c8a3..d8e529c 100644
--- a/src-electron/db/query-endpoint.js
+++ b/src-electron/db/query-endpoint.js
@@ -283,6 +283,7 @@
A.IS_OPTIONAL,
A.DEFINE,
A.API_MATURITY,
+ A.STORAGE_POLICY,
EA.STORAGE_OPTION,
EA.SINGLETON,
EA.BOUNDED,
@@ -326,6 +327,7 @@
min: row.MIN,
max: row.MAX,
storage: row.STORAGE_OPTION,
+ storagePolicy: row.STORAGE_POLICY,
isIncluded: dbApi.fromDbBool(row.INCLUDED),
isSingleton: dbApi.fromDbBool(row.SINGLETON),
isBound: dbApi.fromDbBool(row.BOUNDED),
diff --git a/src-electron/db/query-impexp.js b/src-electron/db/query-impexp.js
index 129cb0e..dd054e5 100644
--- a/src-electron/db/query-impexp.js
+++ b/src-electron/db/query-impexp.js
@@ -764,7 +764,11 @@
}
if (storagePolicy == dbEnum.storagePolicy.attributeAccessInterface) {
attribute.storageOption = dbEnum.storageOption.external
- attribute.defaultValue = null
+ if (
+ !queryUpgrade.keepsDefault(forcedExternal, cluster.name, attributeName)
+ ) {
+ attribute.defaultValue = null
+ }
}
let arg = [
diff --git a/src-electron/db/query-package.js b/src-electron/db/query-package.js
index 954a9f6..caba4a4 100644
--- a/src-electron/db/query-package.js
+++ b/src-electron/db/query-package.js
@@ -704,25 +704,23 @@
* for more flexible queries.
*
* @param {Object} db - The database connection object.
- * @param {string} code - The option code or storage policy code to query for.
+ * @param {string|Array<string>} code - The option code or storage policy code(s) to query for. Can be a single code or an array of codes.
* @param {number|Array<number>} packageIds - The ID(s) of the package(s) to which the options are related. Can be a single ID or an array of IDs.
* @returns {Promise<Array>} A promise that resolves to an array of option objects, each containing the option category, code, and label.
*/
async function getAttributeAccessInterface(db, code, packageIds) {
try {
- // Ensure packageIds is always an array
+ // Ensure code and packageIds are always arrays
+ let codes = Array.isArray(code) ? code : [code]
if (!Array.isArray(packageIds)) {
packageIds = [packageIds]
}
- let packageRefCondition = `po.PACKAGE_REF = ?`
- let attributePackageRefCondition = `a.PACKAGE_REF = ?`
- let queryParams = [code, ...packageIds, code, ...packageIds]
-
- // Since packageIds is now always an array, adjust the query and parameters accordingly
- const placeholders = packageIds.map(() => '?').join(', ')
- packageRefCondition = `po.PACKAGE_REF IN (${placeholders})`
- attributePackageRefCondition = `a.PACKAGE_REF IN (${placeholders})`
+ const codePlaceholders = codes.map(() => '?').join(', ')
+ const packagePlaceholders = packageIds.map(() => '?').join(', ')
+ let packageRefCondition = `po.PACKAGE_REF IN (${packagePlaceholders})`
+ let attributePackageRefCondition = `a.PACKAGE_REF IN (${packagePlaceholders})`
+ let queryParams = [...codes, ...packageIds, ...codes, ...packageIds]
const extendedQuery = `
SELECT
@@ -732,7 +730,7 @@
FROM
PACKAGE_OPTION po
WHERE
- po.OPTION_CODE = ?
+ po.OPTION_CODE IN (${codePlaceholders})
AND ${packageRefCondition}
UNION
@@ -745,7 +743,7 @@
ATTRIBUTE a
LEFT JOIN CLUSTER c ON a.CLUSTER_REF = c.CLUSTER_ID
WHERE
- a.STORAGE_POLICY = ?
+ a.STORAGE_POLICY IN (${codePlaceholders})
AND ${attributePackageRefCondition}
`
diff --git a/src-electron/generator/helper-endpointconfig.js b/src-electron/generator/helper-endpointconfig.js
index c19f5d1..5a3d358 100644
--- a/src-electron/generator/helper-endpointconfig.js
+++ b/src-electron/generator/helper-endpointconfig.js
@@ -30,6 +30,7 @@
const types = require('../util/types.js')
const zclUtil = require('../util/zcl-util.js')
const dbEnum = require('../../src-shared/db-enum.js')
+const matterSdk = require('../sdk/matter.js')
/**
* Returns number of endpoint types.
*
@@ -870,6 +871,67 @@
}
/**
+ * Builds the key under which a cluster/attribute pair is looked up in the set
+ * of attributes that keep their default value.
+ *
+ * @param {*} clusterName
+ * @param {*} attributeName
+ * @returns the lookup key
+ */
+function keepDefaultValueKey(clusterName, attributeName) {
+ return `${clusterName}||${attributeName}`.toLowerCase()
+}
+
+/**
+ * Collects the cluster/attribute pairs whose storage policy forces external
+ * storage but keeps the default value under ZAP control.
+ *
+ * The storage policy of a global attribute, such as FeatureMap, is not on the
+ * attribute itself: there is one attribute row shared by every cluster, so the
+ * policy is recorded per cluster/attribute pair instead. That is why this is
+ * looked up by name rather than read off the attribute.
+ *
+ * @param {*} db
+ * @param {*} zclPackageIds
+ * @returns a set of keys built by keepDefaultValueKey
+ */
+async function collectAttributesKeepingDefaultValue(db, zclPackageIds) {
+ let forcedExternal = await matterSdk.getForcedExternalStorage(
+ db,
+ zclPackageIds
+ )
+ let keys = new Set()
+ forcedExternal.forEach((option) => {
+ if (
+ option.optionCategory != null &&
+ option.optionCode == dbEnum.keepDefaultOption
+ ) {
+ keys.add(keepDefaultValueKey(option.optionCategory, option.optionLabel))
+ }
+ })
+ return keys
+}
+
+/**
+ * Tells whether an external attribute keeps its default value, which means that
+ * the default value is generated even though the attribute takes up no space in
+ * the attribute store.
+ *
+ * @param {*} attribute
+ * @param {*} cluster
+ * @param {*} options collectAttributes options
+ * @returns true if the default value is kept
+ */
+function keepsDefaultValueForExternalStorage(attribute, cluster, options) {
+ return (
+ options.attributesKeepingDefaultValue != null &&
+ options.attributesKeepingDefaultValue.has(
+ keepDefaultValueKey(cluster.name, attribute.name)
+ )
+ )
+}
+
+/**
* Attribute collection works like this:
* 1.) Go over all the clusters that exist.
* 2.) If client is included on at least one endpoint add client atts.
@@ -996,7 +1058,6 @@
// the read/write store.
if (a.storage == dbEnum.storageOption.external) {
storageSize = 0
- defaultSize = 0
// Some external attributes do not have a usable typeSize
// (e.g. structs or lists of structs); the value of typeSize in those
// cases is an error string. Use 0 in those cases.
@@ -1010,7 +1071,19 @@
if (a.typeInfo.atomicType == 'array') {
typeSize = 0
}
- attributeDefaultValue = undefined
+ // Some external attributes keep their default value: their
+ // implementation reads it out of the generated (read only) data
+ // instead of holding it itself. Those still take up space in the
+ // default store, but never in the read/write store. A default value
+ // can only be kept when its size is known, and the two cases above
+ // are exactly the cases where it is not.
+ if (
+ typeSize == 0 ||
+ !keepsDefaultValueForExternalStorage(a, c, options)
+ ) {
+ defaultSize = 0
+ attributeDefaultValue = undefined
+ }
}
let defaultValueIsMacro = false
@@ -1514,15 +1587,20 @@
.then((endpointTypes) =>
collectAttributeSizes(db, this.global.zclPackageIds, endpointTypes)
)
- .then((endpointTypes) =>
- collectAttributes(
+ .then(async (endpointTypes) => {
+ collectAttributesOptions.attributesKeepingDefaultValue =
+ await collectAttributesKeepingDefaultValue(
+ db,
+ this.global.zclPackageIds
+ )
+ return collectAttributes(
db,
sessionId,
endpointTypes,
collectAttributesOptions,
this.global.zclPackageIds
)
- )
+ })
.then((collection) => {
Object.assign(newContext, collection)
})
diff --git a/src-electron/sdk/matter.js b/src-electron/sdk/matter.js
index 223c878..b299db0 100644
--- a/src-electron/sdk/matter.js
+++ b/src-electron/sdk/matter.js
@@ -31,6 +31,12 @@
* Utilizes the attribute access interface to query storage policies
* associated with the specified package ID.
*
+ * Returns both the attributes that are forced to external storage and the
+ * subset of those whose default value stays under ZAP control. The optionCode
+ * of each returned entry says which one it is:
+ * - attributeAccessInterface: forced to External storage
+ * - keepDefault: the default value is not cleared
+ *
* @param {Object} db - Database connection object.
* @param {Number} packageIds - The ID of the packages to query.
* @returns {Promise<Array>} A promise that resolves to an array of forced external storage settings.
@@ -44,7 +50,7 @@
let forcedExternal = await queryPackage.getAttributeAccessInterface(
db,
- dbEnum.storagePolicy.attributeAccessInterface,
+ [dbEnum.storagePolicy.attributeAccessInterface, dbEnum.keepDefaultOption],
packageIdsArray
)
return forcedExternal
@@ -55,6 +61,43 @@
}
/**
+ * Tells whether a cluster/attribute pair from getForcedExternalStorage is
+ * forced to external storage.
+ *
+ * @param {Array} forcedExternal - An array of external options.
+ * @param {String} clusterName - The name of the cluster.
+ * @param {String} attributeName - The name of the attribute.
+ * @returns {boolean}
+ */
+function isForcedExternal(forcedExternal, clusterName, attributeName) {
+ return forcedExternal.some(
+ (option) =>
+ option.optionCategory == clusterName &&
+ option.optionLabel == attributeName &&
+ option.optionCode == dbEnum.storagePolicy.attributeAccessInterface
+ )
+}
+
+/**
+ * Tells whether a cluster/attribute pair from getForcedExternalStorage keeps
+ * its default value. The attribute is still External; only the default is left
+ * alone.
+ *
+ * @param {Array} forcedExternal - An array of external options.
+ * @param {String} clusterName - The name of the cluster.
+ * @param {String} attributeName - The name of the attribute.
+ * @returns {boolean}
+ */
+function keepsDefault(forcedExternal, clusterName, attributeName) {
+ return forcedExternal.some(
+ (option) =>
+ option.optionCategory == clusterName &&
+ option.optionLabel == attributeName &&
+ option.optionCode == dbEnum.keepDefaultOption
+ )
+}
+
+/**
* This function takes a clusterId (the database ID, not the specification-defined ID), an array of attributes (associated with the database defined clusterID),
* and a packageId to identify the specific package the attributes belong to. It changes the global attributes (attributes with specification defined clusterId = null) to represent storage policy
* based on the cluster/attribute pair in zcl.json.
@@ -83,16 +126,10 @@
attributes.map(async (attribute) => {
if (attribute.clusterId == null) {
forcedExternal = await getForcedExternalStorage(db, packageIds)
- forcedExternal.some((option) => {
- if (
- option.optionCategory == clusterName &&
- option.optionLabel == attribute.name
- ) {
- attribute.storagePolicy =
- dbEnum.storagePolicy.attributeAccessInterface
- return true
- }
- })
+ if (isForcedExternal(forcedExternal, clusterName, attribute.name)) {
+ attribute.storagePolicy =
+ dbEnum.storagePolicy.attributeAccessInterface
+ }
}
return attribute
})
@@ -156,15 +193,9 @@
) {
try {
let clusterName = await queryCluster.selectClusterName(db, clusterRef)
- forcedExternal.some((option) => {
- if (
- option.optionCategory == clusterName &&
- option.optionLabel == attributeName
- ) {
- storagePolicy = dbEnum.storagePolicy.attributeAccessInterface
- return true
- }
- })
+ if (isForcedExternal(forcedExternal, clusterName, attributeName)) {
+ storagePolicy = dbEnum.storagePolicy.attributeAccessInterface
+ }
return storagePolicy
} catch (error) {
console.error('Error computing storage policy new config:', error)
@@ -196,16 +227,9 @@
) {
try {
let updatedStoragePolicy = storagePolicy
- forcedExternal.some((option) => {
- if (
- option.optionCategory == clusterName &&
- option.optionLabel == attributeName
- ) {
- updatedStoragePolicy = dbEnum.storagePolicy.attributeAccessInterface
- return true
- }
- return false
- })
+ if (isForcedExternal(forcedExternal, clusterName, attributeName)) {
+ updatedStoragePolicy = dbEnum.storagePolicy.attributeAccessInterface
+ }
return updatedStoragePolicy
} catch (error) {
console.error('Error computing storage import:', error)
@@ -214,6 +238,8 @@
}
exports.getForcedExternalStorage = getForcedExternalStorage
+exports.isForcedExternal = isForcedExternal
+exports.keepsDefault = keepsDefault
exports.computeStorageImport = computeStorageImport
exports.computeStoragePolicyNewConfig = computeStoragePolicyNewConfig
exports.computeStorageOptionNewConfig = computeStorageOptionNewConfig
diff --git a/src-electron/validation/validation.js b/src-electron/validation/validation.js
index 79a8b7c..4943ccb 100644
--- a/src-electron/validation/validation.js
+++ b/src-electron/validation/validation.js
@@ -30,6 +30,7 @@
const env = require('../util/env')
const queryNotification = require('../db/query-package-notification.js')
const dbEnum = require('../../src-shared/db-enum.js')
+const matterSdk = require('../sdk/matter.js')
/**
* Main attribute validation function.
@@ -66,10 +67,6 @@
return { defaultValue: ['Attribute not found in endpoint configuration'] }
}
- if (endpointAttribute.storageOption === dbEnum.storageOption.external) {
- return { defaultValue: [] }
- }
-
let attribute = await queryZcl.selectAttributeById(db, attributeRef)
// Null check for attribute
if (!attribute) {
@@ -79,6 +76,26 @@
)
return { defaultValue: ['Attribute definition not found'] }
}
+
+ // External attributes have no default value to validate, unless they were
+ // asked to keep it, in which case it is editable and it is generated.
+ if (endpointAttribute.storageOption === dbEnum.storageOption.external) {
+ let packages = await queryPackage.getSessionPackages(db, zapSessionId)
+ let forcedExternal = await matterSdk.getForcedExternalStorage(
+ db,
+ packages.map((p) => p.id)
+ )
+ let cluster = await queryZcl.selectClusterById(db, clusterRef)
+ if (
+ !matterSdk.keepsDefault(
+ forcedExternal,
+ cluster ? cluster.name : null,
+ attribute.name
+ )
+ ) {
+ return { defaultValue: [] }
+ }
+ }
return validateSpecificAttribute(
endpointAttribute,
attribute,
diff --git a/src-electron/zcl/zcl-loader-silabs.js b/src-electron/zcl/zcl-loader-silabs.js
index 8784617..ecd473a 100644
--- a/src-electron/zcl/zcl-loader-silabs.js
+++ b/src-electron/zcl/zcl-loader-silabs.js
@@ -43,6 +43,75 @@
const conformParser = require('../validation/conformance-xml-parser')
/**
+ * Normalizes the attributeAccessInterfaceAttributes section of the metadata
+ * file into a list of { name, keepDefault } objects per cluster.
+ *
+ * An entry is either the name of an attribute, which means that the attribute
+ * is handled entirely by the attribute access interface:
+ *
+ * "Access Control": [ "SubjectsPerAccessControlEntry" ]
+ *
+ * or an object that names the attribute and asks for its default value to stay
+ * under ZAP control:
+ *
+ * "Access Control": [ { "name": "FeatureMap", "keepDefault": true } ]
+ *
+ * Both are forced to external storage. The second form does not change that:
+ * it only leaves the default value editable and generated, so the
+ * implementation can read it out of the generated endpoint configuration
+ * instead of holding it itself.
+ *
+ * @param {*} attributeAccessInterfaceAttributes contents of the metadata section
+ * @returns the same clusters, with every entry turned into an object
+ */
+function normalizeAttributeAccessInterfaceAttributes(
+ attributeAccessInterfaceAttributes
+) {
+ let knownKeys = ['name', 'keepDefault']
+ let normalized = {}
+ for (let clusterName of Object.keys(attributeAccessInterfaceAttributes)) {
+ let entries = attributeAccessInterfaceAttributes[clusterName]
+ if (!Array.isArray(entries)) {
+ throw new Error(
+ `\n\nattributeAccessInterfaceAttributes["${clusterName}"] must be an array of attributes\n\n`
+ )
+ }
+ normalized[clusterName] = entries.map((entry) => {
+ if (typeof entry == 'string') {
+ return {
+ name: entry,
+ keepDefault: false
+ }
+ }
+ if (entry == null || typeof entry != 'object' || !('name' in entry)) {
+ throw new Error(
+ `\n\nInvalid entry ${JSON.stringify(
+ entry
+ )} in attributeAccessInterfaceAttributes["${clusterName}"]: expected an attribute name, or an object with a "name" key\n\n`
+ )
+ }
+ let unknownKeys = Object.keys(entry).filter((k) => !knownKeys.includes(k))
+ if (unknownKeys.length > 0) {
+ throw new Error(
+ `\n\nUnknown key(s) ${unknownKeys
+ .map((k) => `"${k}"`)
+ .join(
+ ', '
+ )} for attribute "${entry.name}" in attributeAccessInterfaceAttributes["${clusterName}"]. Known keys are: ${knownKeys
+ .map((k) => `"${k}"`)
+ .join(', ')}\n\n`
+ )
+ }
+ return {
+ name: entry.name,
+ keepDefault: entry.keepDefault === true
+ }
+ })
+ }
+ return normalized
+}
+
+/**
* Promises to read the JSON file and resolve all the data.
* @param {*} ctx Context containing information about the file
* @returns Promise of resolved file.
@@ -129,7 +198,9 @@
if ('attributeAccessInterfaceAttributes' in obj) {
returnObject.attributeAccessInterfaceAttributes =
- obj.attributeAccessInterfaceAttributes
+ normalizeAttributeAccessInterfaceAttributes(
+ obj.attributeAccessInterfaceAttributes
+ )
}
if ('mandatoryDeviceTypes' in obj) {
returnObject.mandatoryDeviceTypes = obj.mandatoryDeviceTypes
@@ -678,12 +749,16 @@
attribute.$.type = 'array'
}
let storagePolicy = dbEnum.storagePolicy.any
- if (context.listsUseAttributeAccessInterface && attribute.$.entryType) {
+ // An entry in attributeAccessInterfaceAttributes names this attribute
+ // specifically, so it wins over the blanket rule for list types.
+ let aaiEntry = (
+ context.attributeAccessInterfaceAttributes?.[cluster.name] ?? []
+ ).find((e) => e.name == name)
+ if (aaiEntry) {
storagePolicy = dbEnum.storagePolicy.attributeAccessInterface
} else if (
- context.attributeAccessInterfaceAttributes &&
- context.attributeAccessInterfaceAttributes[cluster.name] &&
- context.attributeAccessInterfaceAttributes[cluster.name].includes(name)
+ context.listsUseAttributeAccessInterface &&
+ attribute.$.entryType
) {
storagePolicy = dbEnum.storagePolicy.attributeAccessInterface
}
@@ -2620,7 +2695,8 @@
* @param {*} db - The database connection object.
* @param {*} pkgRef - The package reference id for which the attributes are being parsed.
* @param {*} attributeAccessInterfaceAttributes - An object containing the attribute access interface attributes,
- * structured by cluster.
+ * structured by cluster, as returned by
+ * normalizeAttributeAccessInterfaceAttributes.
* @returns {Promise<void>} A promise that resolves when all attributes have been processed and inserted.
*/
async function parseattributeAccessInterfaceAttributes(
@@ -2632,11 +2708,22 @@
for (let i = 0; i < clusters.length; i++) {
const cluster = clusters[i]
const values = attributeAccessInterfaceAttributes[cluster]
- // Prepare the data for insertion
- const optionsKeyValues = values.map((attribute) => ({
- code: dbEnum.storagePolicy.attributeAccessInterface,
- label: attribute
- }))
+ // Prepare the data for insertion. Every entry is still
+ // attributeAccessInterface / External. A second option records that the
+ // default value should stay under ZAP control.
+ const optionsKeyValues = []
+ values.forEach((attribute) => {
+ optionsKeyValues.push({
+ code: dbEnum.storagePolicy.attributeAccessInterface,
+ label: attribute.name
+ })
+ if (attribute.keepDefault) {
+ optionsKeyValues.push({
+ code: dbEnum.keepDefaultOption,
+ label: attribute.name
+ })
+ }
+ })
// Insert the data into the database
try {
await queryPackage.insertOptionsKeyValues(
@@ -3081,12 +3168,10 @@
known_cluster.id,
ctx.packageId
)
- for (let attrName of ctx.attributeAccessInterfaceAttributes[
- clusterName
- ]) {
- if (!known_cluster_attributes.find((a) => a.name == attrName)) {
+ for (let entry of ctx.attributeAccessInterfaceAttributes[clusterName]) {
+ if (!known_cluster_attributes.find((a) => a.name == entry.name)) {
throw new Error(
- `\n\nUnknown attribute "${attrName}" in attributeAccessInterfaceAttributes["${clusterName}"]\n\n`
+ `\n\nUnknown attribute "${entry.name}" in attributeAccessInterfaceAttributes["${clusterName}"]\n\n`
)
}
}
@@ -3133,5 +3218,7 @@
exports.loadIndividualSilabsFile = loadIndividualSilabsFile
exports.loadZclJson = loadZclJson
+exports.normalizeAttributeAccessInterfaceAttributes =
+ normalizeAttributeAccessInterfaceAttributes
exports.loadZclProperties = loadZclProperties
exports.processStructItems = processStructItems
diff --git a/src-shared/db-enum.js b/src-shared/db-enum.js
index da32805..7a0f104 100644
--- a/src-shared/db-enum.js
+++ b/src-shared/db-enum.js
@@ -197,6 +197,11 @@
}
}
+// Package option code for attributeAccessInterfaceAttributes entries that
+// ask for the default value to stay under ZAP control. This is not a storage
+// policy: those attributes are still attributeAccessInterface / External.
+exports.keepDefaultOption = 'keepDefault'
+
exports.storagePolicy = storagePolicy
// When SDK supports a custom device, these are the default values for it.
diff --git a/src/components/ZclAttributeManager.vue b/src/components/ZclAttributeManager.vue
index 1a32bf4..0142ae7 100644
--- a/src/components/ZclAttributeManager.vue
+++ b/src/components/ZclAttributeManager.vue
@@ -186,12 +186,26 @@
? 'grey'
: ''
"
- :disable="isDisabledDefault(props.row.id, selectedCluster.id)"
+ :disable="
+ isDisabledDefault(
+ props.row.id,
+ props.row.label,
+ selectedCluster.id
+ )
+ "
:model-value="
props.row.isNullable &&
- defaultValueCheck(props.row.id, selectedCluster.id) === null
+ defaultValueCheck(
+ props.row.id,
+ props.row.label,
+ selectedCluster.id
+ ) === null
? 'NULL'
- : defaultValueCheck(props.row.id, selectedCluster.id)
+ : defaultValueCheck(
+ props.row.id,
+ props.row.label,
+ selectedCluster.id
+ )
"
:error="
!isDefaultValueValid(
@@ -263,15 +277,28 @@
)
})
},
+ /* Attributes that are forced external can still keep their default value
+ under ZAP control, in which case the default stays editable even though
+ the attribute takes up no space in the attribute store. */
+ checkKeepDefault(name) {
+ return this.forcedExternal.some((option) => {
+ return (
+ option.optionCategory == this.selectedCluster.label &&
+ option.optionLabel == name &&
+ option.optionCode == DbEnum.keepDefaultOption
+ )
+ })
+ },
//return true and disable default field if Storage is External AND if attribute is not enabled
- isDisabledDefault(id, selectedClusterId) {
+ isDisabledDefault(id, name, selectedClusterId) {
return (
!this.selection.includes(
this.hashAttributeIdClusterId(id, selectedClusterId)
) ||
- this.selectionStorageOption[
+ (this.selectionStorageOption[
this.hashAttributeIdClusterId(id, selectedClusterId)
- ] == 'External'
+ ] == 'External' &&
+ !this.checkKeepDefault(name))
)
},
//return true and disable Storage if forced External AND if attribute is not enabled
@@ -289,8 +316,8 @@
)
},
//if disabled return null to be set as the default value
- defaultValueCheck(id, selectedClusterId) {
- if (this.isDisabledDefault(id, selectedClusterId)) {
+ defaultValueCheck(id, name, selectedClusterId) {
+ if (this.isDisabledDefault(id, name, selectedClusterId)) {
return null
} else {
return this.selectionDefault[
diff --git a/test/aai-keep-default.test.js b/test/aai-keep-default.test.js
new file mode 100644
index 0000000..63fe5dc
--- /dev/null
+++ b/test/aai-keep-default.test.js
@@ -0,0 +1,235 @@
+/**
+ *
+ * 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.
+ *
+ *
+ * @jest-environment node
+ */
+
+const genEngine = require('../src-electron/generator/generation-engine')
+const env = require('../src-electron/util/env')
+const dbApi = require('../src-electron/db/db-api')
+const zclLoader = require('../src-electron/zcl/zcl-loader')
+const zclLoaderSilabs = require('../src-electron/zcl/zcl-loader-silabs')
+const importJs = require('../src-electron/importexport/import')
+const querySession = require('../src-electron/db/query-session')
+const queryPackage = require('../src-electron/db/query-package')
+const queryEndpointType = require('../src-electron/db/query-endpoint-type')
+const queryZcl = require('../src-electron/db/query-zcl')
+const matterSdk = require('../src-electron/sdk/matter')
+const dbEnum = require('../src-shared/db-enum')
+const testUtil = require('./test-util')
+
+let db
+let zclPackageId
+let templateContext
+
+// The cluster/attribute pair that this test asks to keep its default value.
+// FeatureMap is the canonical case: it is still External, so it needs no room
+// in the attribute store, but its value is a fixed bitmap that the
+// implementation can read out of the generated defaults.
+const keepDefaultCluster = 'Software Diagnostics'
+const keepDefaultAttribute = 'FeatureMap'
+
+beforeAll(async () => {
+ env.setDevelopmentEnv()
+ let file = env.sqliteTestFile('aai-keep-default')
+ db = await dbApi.initDatabaseAndLoadSchema(
+ file,
+ env.schemaFile(),
+ env.zapVersion()
+ )
+ let ctx = await zclLoader.loadZcl(db, env.builtinMatterZclMetafile())
+ zclPackageId = ctx.packageId
+}, testUtil.timeout.medium())
+
+afterAll(() => dbApi.closeDatabase(db), testUtil.timeout.short())
+
+test('Plain entries of attributeAccessInterfaceAttributes keep their meaning', () => {
+ let normalized = zclLoaderSilabs.normalizeAttributeAccessInterfaceAttributes({
+ 'Access Control': ['ClusterRevision', 'AccessControlEntriesPerFabric']
+ })
+ expect(normalized['Access Control']).toEqual([
+ { name: 'ClusterRevision', keepDefault: false },
+ { name: 'AccessControlEntriesPerFabric', keepDefault: false }
+ ])
+})
+
+test('keepDefault is a flag on the same External attribute, not a new storage policy', () => {
+ let normalized = zclLoaderSilabs.normalizeAttributeAccessInterfaceAttributes({
+ 'Access Control': [
+ 'ClusterRevision',
+ { name: 'FeatureMap', keepDefault: true },
+ { name: 'AccessControlEntriesPerFabric', keepDefault: false }
+ ]
+ })
+ expect(normalized['Access Control']).toEqual([
+ { name: 'ClusterRevision', keepDefault: false },
+ { name: 'FeatureMap', keepDefault: true },
+ { name: 'AccessControlEntriesPerFabric', keepDefault: false }
+ ])
+})
+
+test('Malformed entries of attributeAccessInterfaceAttributes are rejected', () => {
+ expect(() =>
+ zclLoaderSilabs.normalizeAttributeAccessInterfaceAttributes({
+ 'Access Control': 'FeatureMap'
+ })
+ ).toThrow(/must be an array/)
+ expect(() =>
+ zclLoaderSilabs.normalizeAttributeAccessInterfaceAttributes({
+ 'Access Control': [{ keepDefault: true }]
+ })
+ ).toThrow(/expected an attribute name/)
+ // A typo in the flag would otherwise silently do nothing.
+ expect(() =>
+ zclLoaderSilabs.normalizeAttributeAccessInterfaceAttributes({
+ 'Access Control': [{ name: 'FeatureMap', keepDefaults: true }]
+ })
+ ).toThrow(/Unknown key\(s\) "keepDefaults"/)
+})
+
+test(
+ 'A cluster/attribute pair that keeps its default is still External',
+ async () => {
+ // This is what the loader records for a metadata file that asks for the
+ // default value of this cluster/attribute pair to be kept. The pair is
+ // already listed as a plain attribute access interface attribute by the
+ // built in Matter metadata, so it is already forced External. The extra
+ // option only says not to clear the default.
+ await queryPackage.insertOptionsKeyValues(
+ db,
+ zclPackageId,
+ keepDefaultCluster,
+ [
+ {
+ code: dbEnum.keepDefaultOption,
+ label: keepDefaultAttribute
+ }
+ ]
+ )
+
+ let forcedExternal = await matterSdk.getForcedExternalStorage(
+ db,
+ zclPackageId
+ )
+ expect(
+ matterSdk.isForcedExternal(
+ forcedExternal,
+ keepDefaultCluster,
+ keepDefaultAttribute
+ )
+ ).toBe(true)
+ expect(
+ matterSdk.keepsDefault(
+ forcedExternal,
+ keepDefaultCluster,
+ keepDefaultAttribute
+ )
+ ).toBe(true)
+ // Everything else in that cluster is unaffected.
+ expect(
+ matterSdk.isForcedExternal(
+ forcedExternal,
+ keepDefaultCluster,
+ 'CurrentHeapFree'
+ )
+ ).toBe(true)
+ expect(
+ matterSdk.keepsDefault(
+ forcedExternal,
+ keepDefaultCluster,
+ 'CurrentHeapFree'
+ )
+ ).toBe(false)
+ expect(
+ matterSdk.keepsDefault(
+ forcedExternal,
+ keepDefaultCluster,
+ 'ThisAttributeDoesNotExist'
+ )
+ ).toBe(false)
+ },
+ testUtil.timeout.medium()
+)
+
+test(
+ 'An attribute that keeps its default is generated with it, and without attribute store space',
+ async () => {
+ templateContext = await genEngine.loadTemplates(
+ db,
+ testUtil.testTemplate.matter3
+ )
+ expect(templateContext.packageId).not.toBeNull()
+
+ let sessionId = await querySession.createBlankSession(db)
+ await importJs.importDataFromFile(db, testUtil.matterTestFile.allClusters, {
+ sessionId: sessionId
+ })
+
+ // The default value survives the import, which is what makes it editable in
+ // the UI and available to generation.
+ let endpointTypes = await queryEndpointType.selectAllEndpointTypes(
+ db,
+ sessionId
+ )
+ let attributes = []
+ for (let et of endpointTypes) {
+ attributes.push(
+ ...(await queryZcl.selectEndpointTypeAttributesByEndpointId(
+ db,
+ et.endpointTypeId
+ ))
+ )
+ }
+ let featureMaps = []
+ for (let a of attributes) {
+ if (a.storageOption != dbEnum.storageOption.external) continue
+ let definition = await queryZcl.selectAttributeById(db, a.attributeRef)
+ let cluster = await queryZcl.selectClusterById(db, a.clusterRef)
+ if (
+ definition.name == keepDefaultAttribute &&
+ cluster.name == keepDefaultCluster
+ ) {
+ featureMaps.push(a)
+ }
+ }
+ expect(featureMaps.length).toBeGreaterThan(0)
+ featureMaps.forEach((a) => expect(a.defaultValue).toEqual('1'))
+
+ let genResult = await genEngine.generate(
+ db,
+ sessionId,
+ templateContext.packageId,
+ {},
+ { disableDeprecationWarnings: true }
+ )
+ expect(genResult.hasErrors).toEqual(false)
+
+ let ept = genResult.content['endpoint_config.h']
+ // The attribute is still external, so it takes up no space in the attribute
+ // store, but the default value is now in the generated data instead of
+ // being zeroed out.
+ expect(ept).toContain(
+ '{ 0x0000FFFC, ZAP_TYPE(BITMAP32), 4, ZAP_ATTRIBUTE_MASK(EXTERNAL_STORAGE), ZAP_SIMPLE_DEFAULT(1) }, /* FeatureMap */'
+ )
+ // Attributes of the same cluster that were not asked to keep their default
+ // still have none.
+ expect(ept).toContain(
+ '{ 0x00000001, ZAP_TYPE(INT64U), 8, ZAP_ATTRIBUTE_MASK(EXTERNAL_STORAGE), ZAP_EMPTY_DEFAULT() }, /* CurrentHeapFree */'
+ )
+ },
+ testUtil.timeout.long()
+)