blob: 7fdf0280f22ce9d5d19f65c884951d67a196f831 [file] [edit]
/**
*
* Copyright (c) 2020 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 the APIs for ZCL/Data-Model loading.
*
* @module Loader API: Loader APIs
*/
const fs = require('fs')
const fsp = fs.promises
const path = require('path')
const properties = require('properties')
const dbApi = require('../db/db-api')
const queryPackage = require('../db/query-package')
const querySession = require('../db/query-session')
const queryDeviceType = require('../db/query-device-type')
const queryLoader = require('../db/query-loader')
const queryZcl = require('../db/query-zcl')
const env = require('../util/env')
const bin = require('../util/bin')
const util = require('../util/util')
const dbEnum = require('../../src-shared/db-enum')
const zclLoader = require('./zcl-loader')
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.
* @param {*} ctx Context containing information about the file
* @returns Promise of resolved file.
*/
async function collectDataFromJsonFile(metadataFile, data) {
env.logDebug(`Collecting ZCL files from JSON file: ${metadataFile}`)
let obj = JSON.parse(data)
let f
let returnObject = {}
let fileLocations
if (Array.isArray(obj.xmlRoot)) {
fileLocations = obj.xmlRoot.map((p) =>
path.join(path.dirname(metadataFile), p)
)
} else {
fileLocations = [path.join(path.dirname(metadataFile), obj.xmlRoot)]
}
let zclFiles = []
obj.xmlFile.forEach((xmlF) => {
f = util.locateRelativeFilePath(fileLocations, xmlF)
if (f != null) zclFiles.push(f)
})
returnObject.zclFiles = zclFiles
// Manufacturers XML file.
f = util.locateRelativeFilePath(fileLocations, obj.manufacturersXml)
if (f != null) returnObject.manufacturersXml = f
// Profiles XML File
f = util.locateRelativeFilePath(fileLocations, obj.profilesXml)
if (f != null) returnObject.profilesXml = f
// Zcl XSD file
f = util.locateRelativeFilePath(fileLocations, obj.zclSchema)
if (f != null) returnObject.zclSchema = f
// General options
// Note that these values when put into OPTION_CODE will generally be converted to lowercase.
if (obj.options) {
returnObject.options = obj.options
}
// Defaults. Note that the keys should be the categories that are listed for PACKAGE_OPTION, and the value should be the OPTION_CODE
if (obj.defaults) {
returnObject.defaults = obj.defaults
}
// Feature Flags
if (obj.featureFlags) {
returnObject.featureFlags = obj.featureFlags
}
if (obj.uiOptions) {
returnObject.uiOptions = obj.uiOptions
}
// Default reportability.
// `defaultReportable` was old thing that could be true or false.
// We still honor it.
returnObject.defaultReportingPolicy =
dbEnum.reportingPolicy.defaultReportingPolicy
if ('defaultReportable' in obj) {
returnObject.defaultReportingPolicy = obj.defaultReportable
? dbEnum.reportingPolicy.suggested
: dbEnum.reportingPolicy.optional
}
// Default reporting policy is the new thing that can be mandatory/optional/suggested/prohibited.
// If it's missing, 'optional' is a default reporting policy.
if ('defaultReportingPolicy' in obj) {
returnObject.defaultReportingPolicy = dbEnum.reportingPolicy.resolve(
obj.defaultReportingPolicy
)
}
returnObject.version = obj.version
returnObject.category = obj.category
returnObject.description = obj.description
returnObject.supportCustomZclDevice = obj.supportCustomZclDevice
if ('listsUseAttributeAccessInterface' in obj) {
returnObject.listsUseAttributeAccessInterface =
obj.listsUseAttributeAccessInterface
}
if ('attributeAccessInterfaceAttributes' in obj) {
returnObject.attributeAccessInterfaceAttributes =
obj.attributeAccessInterfaceAttributes
}
if ('mandatoryDeviceTypes' in obj) {
returnObject.mandatoryDeviceTypes = obj.mandatoryDeviceTypes
}
if ('ZCLDataTypes' in obj) {
returnObject.ZCLDataTypes = obj.ZCLDataTypes
} else {
returnObject.ZCLDataTypes = [
'ARRAY',
'BITMAP',
'ENUM',
'NUMBER',
'STRING',
'STRUCT'
]
}
if ('zcl' in obj) {
returnObject.zcl = obj.zcl
}
// zcl.json can contain 'fabricHandling' toplevel key. It is expected
// to look like this:
// "fabricHandling": {
// "automaticallyCreateFields": true,
// "indexFieldId": 254,
// "indexFieldName": "FabricIndex",
// "indexType": "fabric_idx"
// },
//
// If this configuration is present, then special logic to automatically
// add fabric index field to fabric-sensitive and fabric-scoped
// things will kick in.
//
// If this field is not present then the special logic will not
// happen.
if ('fabricHandling' in obj) {
returnObject.fabricHandling = obj.fabricHandling
} else {
returnObject.fabricHandling = {
automaticallyCreateFields: false
}
}
if ('newXmlFile' in obj) {
returnObject.newXmlFile = obj.newXmlFile.map((f) =>
path.join(path.dirname(metadataFile), f)
)
} else {
returnObject.newXmlFile = []
}
env.logDebug(
`Resolving: ${returnObject.zclFiles}, version: ${returnObject.version}`
)
return returnObject
}
/**
* Promises to read the properties file, extract all the actual xml files, and resolve with the array of files.
*
* @param {*} ctx Context which contains information about the propertiesFiles and data
* @returns Promise of resolved files.
*/
async function collectDataFromPropertiesFile(metadataFile, data) {
return new Promise((resolve, reject) => {
env.logDebug(`Collecting ZCL files from properties file: ${metadataFile}`)
let returnObject = {}
properties.parse(data, { namespaces: true }, (err, zclProps) => {
if (err) {
env.logError(`Could not read file: ${metadataFile}`)
reject(util.toErrorObject(err))
} else {
let fileLocations = zclProps.xmlRoot
.split(',')
.map((p) => path.join(path.dirname(metadataFile), p))
let zclFiles = []
let f
// Iterate over all XML files in the properties file, and check
// if they exist in one or the other directory listed in xmlRoot
zclProps.xmlFile.split(',').forEach((singleXmlFile) => {
let fullPath = util.locateRelativeFilePath(
fileLocations,
singleXmlFile
)
if (fullPath != null) zclFiles.push(fullPath)
})
returnObject.zclFiles = zclFiles
// Manufacturers XML file.
f = util.locateRelativeFilePath(
fileLocations,
zclProps.manufacturersXml
)
if (f != null) returnObject.manufacturersXml = f
// Profiles XML file.
f = util.locateRelativeFilePath(fileLocations, zclProps.profilesXml)
if (f != null) returnObject.profilesXml = f
// Zcl XSD file
f = util.locateRelativeFilePath(fileLocations, zclProps.zclSchema)
if (f != null) returnObject.zclSchema = f
// General options
// Note that these values when put into OPTION_CODE will generally be converted to lowercase.
if (zclProps.options) {
returnObject.options = zclProps.options
}
// Defaults. Note that the keys should be the categories that are listed for PACKAGE_OPTION, and the value should be the OPTION_CODE
if (zclProps.defaults) {
returnObject.defaults = zclProps.defaults
}
// Feature Flags
if (zclProps.featureFlags) {
returnObject.featureFlags = zclProps.featureFlags
}
// ZCLDataTypes
if (zclProps.zclDataTypes) {
returnObject.ZCLDataTypes = zclProps.ZCLDataTypes
} else {
returnObject.ZCLDataTypes = [
'ARRAY',
'BITMAP',
'ENUM',
'NUMBER',
'STRING',
'STRUCT'
]
}
returnObject.supportCustomZclDevice = zclProps.supportCustomZclDevice
returnObject.version = zclProps.version
returnObject.description = zclProps.description
returnObject.category = zclProps.category
// Don't bother with allowing this in the properties file.
// It's legacy only.
returnObject.fabricHandling = {
automaticallyCreateFields: false
}
env.logDebug(
`Resolving: ${returnObject.zclFiles}, version: ${returnObject.version}`
)
resolve(returnObject)
}
})
})
}
/**
* Silabs XML does not carry types with bitmap fields, but dotdot does, so they are in the schema.
* Just to put some data in, we differentiate between "bool" and "enum" types here.
*
* @param {*} mask
* @returns bool or corresponding enum
*/
function maskToType(mask) {
let n = parseInt(mask)
let bitCount = bin.bitCount(n)
if (bitCount <= 1) {
return 'bool'
} else if (bitCount <= 8) {
return 'enum8'
} else if (bitCount <= 16) {
return 'enum16'
} else {
return 'enum32'
}
}
/**
* Prepare atomic to db insertion.
*
* @param {*} a
*/
function prepareAtomic(a) {
return {
name: a.$.name,
id: parseInt(a.$.id),
size: a.$.size,
description: a.$.description,
isDiscrete: a.$.discrete == 'true',
isComposite: a.$.composite == 'true',
isSigned: a.$.signed == 'true',
isString:
a.$.string == 'true' ||
a.$.name.toLowerCase() == 'char_string' ||
a.$.name.toLowerCase() == 'long_char_string' ||
a.$.name.toLowerCase() == 'octet_string' ||
a.$.name.toLowerCase() == 'long_octet_string',
isLong:
a.$.long == 'true' ||
a.$.name.toLowerCase() == 'long_char_string' ||
a.$.name.toLowerCase() == 'long_octet_string',
isChar:
a.$.char == 'true' ||
a.$.name.toLowerCase() == 'char_string' ||
a.$.name.toLowerCase() == 'long_char_string',
isFloat:
a.$.float == 'true' ||
a.$.name.toLowerCase() === 'single' ||
a.$.name.toLowerCase() === 'double' ||
a.$.name.toLowerCase() === 'float',
baseType: a.$.baseType
}
}
/**
* Processes atomic types for DB insertion.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} data
* @returns Promise of inserted bitmaps
*/
async function processAtomics(db, filePath, packageId, data) {
let types = data[0].type
env.logDebug(`${filePath}, ${packageId}: ${types.length} atomic types.`)
return queryLoader.insertAtomics(
db,
packageId,
types.map((x) => prepareAtomic(x))
)
}
/**
* Prepares global attribute data.
*
* @param {*} cluster
* @returns Object containing the data from XML.
*/
function prepareClusterGlobalAttribute(cluster) {
if ('globalAttribute' in cluster) {
let ret = {}
ret.code = parseInt(cluster.code[0], 16)
if ('$' in cluster) {
let mfgCode = cluster['$'].manufacturerCode
if (mfgCode != null) ret.manufacturerCode = mfgCode
}
ret.globalAttribute = []
cluster.globalAttribute.forEach((ga) => {
let at = {
code: parseInt(ga.$.code),
value: ga.$.value
}
if ('featureBit' in ga) {
at.featureBit = ga.featureBit.map((fb) => {
let content = fb._ != null ? fb._.toLowerCase() : null
return {
tag: fb.$.tag,
bit: parseInt(fb.$.bit),
value: content == '1' || content == 'true'
}
})
}
if (ga.$.side == dbEnum.side.either) {
ret.globalAttribute.push(
Object.assign({ side: dbEnum.side.client }, at)
)
ret.globalAttribute.push(
Object.assign({ side: dbEnum.side.server }, at)
)
} else {
ret.globalAttribute.push(Object.assign({ side: ga.$.side }, at))
}
})
return ret
} else {
return null
}
}
/**
* Extract access information
* @param {*} ac
* @returns access tag information
*/
function extractAccessTag(ac) {
let e = {
op: ac.$.op,
role: ac.$.role,
modifier: ac.$.modifier
}
if ('privilege' in ac.$) {
e.role = ac.$.privilege
}
return e
}
/**
* Extract list of access information
* @param {*} xmlElement
* @returns array of access information
*/
function extractAccessIntoArray(xmlElement) {
let accessArray = []
if ('access' in xmlElement) {
for (const ac of xmlElement.access) {
accessArray.push(extractAccessTag(ac))
}
}
return accessArray
}
/**
* Check whether any <access fabricSensitive="true"/> child is present on the
* given parsed XML element. For attributes, the CSA data model only uses this
* form (not isFabricSensitive on the attribute start tag), e.g.
* <access read="true" readPrivilege="manage" fabricSensitive="true"/>
*
* @param {*} xmlElement xml2js-parsed element that may contain an `access` array
* @returns Whether any access child has fabricSensitive="true"
*/
function isAccessFabricSensitive(xmlElement) {
if (!('access' in xmlElement)) return false
for (const ac of xmlElement.access) {
if (ac && ac.$ && ac.$.fabricSensitive == 'true') return true
}
return false
}
/**
* Prepare XML cluster for insertion into the database.
* This method can also prepare clusterExtensions.
*
* @param {*} cluster
* @returns Object containing all data from XML.
*/
function prepareCluster(cluster, context, isExtension = false) {
let ret = {
isExtension: isExtension
}
if (isExtension) {
if ('$' in cluster && 'code' in cluster.$) {
ret.code = parseInt(cluster.$.code)
}
} else {
ret.code = parseInt(cluster.code[0])
ret.name = cluster.name[0]
ret.description = cluster.description ? cluster.description[0].trim() : ''
ret.define = cluster.define[0]
// handle domain data parsed from both formats:
// <domain>General</domain> and <domain name="General"/>
if (cluster.domain[0] && cluster.domain[0].$) {
ret.domain = cluster.domain[0].$.name
} else {
ret.domain = cluster.domain[0]
}
ret.isSingleton = false
if ('$' in cluster) {
if (cluster.$.manufacturerCode == null) {
ret.manufacturerCode = null
} else {
ret.manufacturerCode = parseInt(cluster.$.manufacturerCode)
}
if (cluster.$.singleton == 'true') {
ret.isSingleton = true
}
ret.introducedIn = cluster.$.introducedIn
ret.removedIn = cluster.$.removedIn
ret.apiMaturity = cluster.$.apiMaturity
}
}
if ('tag' in cluster) {
ret.tags = cluster.tag.map((tag) => prepareTag(tag))
}
if ('command' in cluster) {
ret.commands = []
cluster.command.forEach((command) => {
let quality = null
if ('quality' in command) {
quality = command.quality[0].$
}
let cmd = {
code: parseInt(command.$.code),
manufacturerCode: command.$.manufacturerCode,
name: command.$.name,
description: command.description ? command.description[0].trim() : '',
source: command.$.source,
isOptional: conformParser.getOptionalAttributeFromXML(
command,
'command'
),
conformance: conformParser.parseConformanceFromXML(command),
mustUseTimedInvoke: command.$.mustUseTimedInvoke == 'true',
introducedIn: command.$.introducedIn,
removedIn: command.$.removedIn,
responseName: command.$.response == null ? null : command.$.response,
isDefaultResponseEnabled:
command.$.disableDefaultResponse == 'true' ? false : true,
isFabricScoped: command.$.isFabricScoped == 'true',
isLargeMessage: quality ? quality.largeMessage == 'true' : false,
apiMaturity: command.$.apiMaturity
}
cmd.access = extractAccessIntoArray(command)
if (cmd.manufacturerCode == null) {
cmd.manufacturerCode = ret.manufacturerCode
} else {
cmd.manufacturerCode = parseInt(cmd.manufacturerCode)
}
if ('arg' in command) {
cmd.args = []
let lastFieldId = -1
command.arg.forEach((arg) => {
let defaultFieldId = lastFieldId + 1
lastFieldId = arg.$.fieldId ? parseInt(arg.$.fieldId) : defaultFieldId
// We are only including ones that are NOT removedIn
if (arg.$.removedIn == null)
cmd.args.push({
name: arg.$.name,
type: arg.$.type,
min: arg.$.min,
max: arg.$.max,
minLength: 0,
maxLength: arg.$.length ? arg.$.length : null,
defaultValue: arg.$.default ? arg.$.default : null,
isArray: arg.$.array == 'true' ? 1 : 0,
presentIf: arg.$.presentIf,
isNullable: arg.$.isNullable == 'true' ? true : false,
isOptional:
arg.$.optional == 'true' || arg.$.optional == '1'
? true
: false,
countArg: arg.$.countArg,
fieldIdentifier: lastFieldId,
introducedIn: arg.$.introducedIn,
removedIn: arg.$.removedIn,
apiMaturity: arg.$.apiMaturity
})
})
}
ret.commands.push(cmd)
})
}
if ('event' in cluster) {
ret.events = []
cluster.event.forEach((event) => {
let ev = {
code: parseInt(event.$.code),
manufacturerCode: event.$.manufacturerCode,
name: event.$.name,
side: event.$.side,
conformance: conformParser.parseConformanceFromXML(event),
priority: event.$.priority,
apiMaturity: event.$.apiMaturity,
description: event.description ? event.description[0].trim() : '',
isOptional: conformParser.getOptionalAttributeFromXML(event, 'event'),
isFabricSensitive: event.$.isFabricSensitive == 'true'
}
ev.access = extractAccessIntoArray(event)
if (ev.manufacturerCode == null) {
ev.manufacturerCode = ret.manufacturerCode
} else {
ev.manufacturerCode = parseInt(ev.manufacturerCode)
}
if ('field' in event) {
ev.fields = []
let lastFieldId = -1
event.field.forEach((field) => {
let defaultFieldId = lastFieldId + 1
const rawFieldId = field.$.fieldId ?? field.$.id
lastFieldId = rawFieldId ? parseInt(rawFieldId) : defaultFieldId
if (field.$.removedIn == null) {
ev.fields.push({
name: field.$.name,
type: field.$.type,
defaultValue: field.$.default ? field.$.default : null,
isArray: field.$.array == 'true' ? 1 : 0,
isNullable: field.$.isNullable == 'true' ? true : false,
isOptional: field.$.optional == 'true' ? true : false,
fieldIdentifier: lastFieldId,
introducedIn: field.$.introducedIn,
removedIn: field.$.removedIn,
apiMaturity: field.$.apiMaturity
})
}
})
}
if (
context.fabricHandling &&
context.fabricHandling.automaticallyCreateFields &&
ev.isFabricSensitive
) {
if (!ev.fields) {
ev.fields = []
}
ev.fields.push({
name: context.fabricHandling.indexFieldName,
type: context.fabricHandling.indexType,
isArray: false,
isNullable: false,
isOptional: false,
fieldIdentifier: context.fabricHandling.indexFieldId,
introducedIn: null,
removedIn: null
})
}
// We only add event if it does not have removedIn
if (ev.removedIn == null) ret.events.push(ev)
})
}
if ('attribute' in cluster) {
ret.attributes = []
cluster.attribute.forEach((attribute) => {
let name = attribute._
let quality = null
if (attribute.$ && name == null) {
name = attribute.$.name
}
if ('description' in attribute && name == null) {
name = attribute.description.join('')
}
if ('quality' in attribute) {
quality = attribute.quality[0].$
}
let reportingPolicy = context.defaultReportingPolicy
if (attribute.$.reportable == 'true') {
reportingPolicy = dbEnum.reportingPolicy.suggested
} else if (attribute.$.reportable == 'false') {
reportingPolicy = dbEnum.reportingPolicy.optional
} else if (attribute.$.reportingPolicy != null) {
reportingPolicy = dbEnum.reportingPolicy.resolve(
attribute.$.reportingPolicy
)
}
/* If the XML uses `array="true" type="X"` to define a list type,
convert it to `type="array" entryType="X"` to support both formats */
if (attribute.$.array == 'true') {
attribute.$.entryType = attribute.$.type
attribute.$.type = 'array'
}
let storagePolicy = dbEnum.storagePolicy.any
if (context.listsUseAttributeAccessInterface && attribute.$.entryType) {
storagePolicy = dbEnum.storagePolicy.attributeAccessInterface
} else if (
context.attributeAccessInterfaceAttributes &&
context.attributeAccessInterfaceAttributes[cluster.name] &&
context.attributeAccessInterfaceAttributes[cluster.name].includes(name)
) {
storagePolicy = dbEnum.storagePolicy.attributeAccessInterface
}
let att = {
code: parseInt(attribute.$.code),
manufacturerCode: attribute.$.manufacturerCode,
name: name,
type:
attribute.$.type.toUpperCase() == attribute.$.type
? attribute.$.type.toLowerCase()
: attribute.$.type,
side: attribute.$.side,
define: attribute.$.define,
conformance: conformParser.parseConformanceFromXML(attribute),
min: attribute.$.min,
max: attribute.$.max,
minLength: 0,
maxLength: attribute.$.length ? attribute.$.length : null,
reportMinInterval: attribute.$.reportMinInterval,
reportMaxInterval: attribute.$.reportMaxInterval,
reportableChange: attribute.$.reportableChange,
reportableChangeLength: attribute.$.reportableChangeLength
? attribute.$.reportableChangeLength
: null,
isWritable: attribute.$.writable == 'true',
isReadable: attribute.$.readable != 'false',
defaultValue: attribute.$.default,
isOptional: conformParser.getOptionalAttributeFromXML(
attribute,
'attribute'
),
reportingPolicy: reportingPolicy,
storagePolicy: storagePolicy,
isSceneRequired:
attribute.$.sceneRequired == 'true' ||
(quality != null && quality.scene == 'true'),
introducedIn: attribute.$.introducedIn,
removedIn: attribute.$.removedIn,
isNullable: attribute.$.isNullable == 'true' ? true : false,
isFabricSensitive: isAccessFabricSensitive(attribute),
entryType: attribute.$.entryType,
mustUseTimedWrite: attribute.$.mustUseTimedWrite == 'true',
apiMaturity: attribute.$.apiMaturity,
isChangeOmitted: quality ? quality.changeOmitted == 'true' : false,
persistence: quality ? quality.persistence : null
}
att.access = extractAccessIntoArray(attribute)
if (att.manufacturerCode == null) {
att.manufacturerCode = ret.manufacturerCode
} else {
att.manufacturerCode = parseInt(att.manufacturerCode)
}
// Setting max length for string type attributes when not specified by
// the xml.
if (
att.type &&
(att.type.toLowerCase() == 'long_octet_string' ||
att.type.toLowerCase() == 'long_char_string') &&
(att.maxLength == 0 || !att.maxLength)
) {
if (context.category == 'zigbee') {
// Setting the max length for long strings to 253 instead of 65534
// if not already set by xml.
env.logWarning(
'Long string max length not set for ' +
att.name +
' in xml. \
Currently defaulting to a max length of 253 for long strings instead of 65534 \
for space conservation and no support available for long strings in zigbee pro.'
)
att.maxLength = 253
} else {
// Setting the max length for long strings to 1024 instead of 65534
// if not already set by xml.
env.logWarning(
'Long string max length not set for ' +
att.name +
' in xml. \
Currently defaulting to a max length of 1024 for long strings instead of 65534 \
for space conservation.'
)
att.maxLength = 1024
}
}
if (
att.type &&
(att.type.toLowerCase() == 'octet_string' ||
att.type.toLowerCase() == 'char_string') &&
(att.maxLength == 0 || !att.maxLength)
) {
att.maxLength = 254
}
// If attribute has removedIn, then it's not valid any more in LATEST spec.
if (att.removedIn == null) ret.attributes.push(att)
})
}
if ('features' in cluster) {
ret.features = []
cluster.features[0].feature.forEach((feature) => {
let f = {
name: feature.$.name,
code: feature.$.code,
bit: feature.$.bit,
defaultValue: feature.$.default,
description: feature.$.summary,
conformance: conformParser.parseConformanceFromXML(feature)
}
ret.features.push(f)
})
}
return ret
}
/**
* Process clusters for insertion into the database.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} data
* @returns Promise of cluster insertion.
*/
async function processClusters(db, filePath, packageId, data, context) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} clusters.`)
// We prepare clusters, but we ignore the ones that have already been loaded.
let preparedClusters = data
.map((x) => prepareCluster(x, context))
.filter((cluster) => {
if (
context.clustersLoadedFromNewFiles &&
context.clustersLoadedFromNewFiles.includes(cluster.code)
) {
env.logDebug(
`Bypassing loading of cluster ${cluster.code} from old files.`
)
return false
} else {
return true
}
})
// and then run the DB process.
return queryLoader.insertClusters(db, packageId, preparedClusters)
}
/**
* Processes global attributes for insertion into the database.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} data
* @returns Promise of inserted data.
*/
function processClusterGlobalAttributes(db, filePath, packageId, data) {
let objs = []
data.forEach((x) => {
let p = prepareClusterGlobalAttribute(x)
if (p != null) objs.push(p)
})
if (objs.length > 0) {
return queryLoader.insertGlobalAttributeDefault(db, packageId, objs)
} else {
return null
}
}
/**
* Cluster Extension contains attributes and commands in a same way as regular cluster,
* and it has an attribute code="0xXYZ" where code is a cluster code.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} data
* @returns promise to resolve the clusterExtension tags
*/
async function processClusterExtensions(
db,
filePath,
dataPackageId,
knownPackages,
data,
context
) {
env.logDebug(
`${filePath}, ${dataPackageId}: ${data.length} cluster extensions.`
)
// Insert cluster extensions
return queryLoader.insertClusterExtensions(
db,
dataPackageId,
knownPackages,
data.map((x) => prepareCluster(x, context, true))
)
}
/**
* Processes the globals in the XML files. The `global` tag contains
* attributes and commands in a same way as cluster or clusterExtension
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} data
* @returns promise to resolve the globals
*/
async function processGlobals(db, filePath, packageId, data, context) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} globals.`)
return queryLoader.insertGlobals(
db,
packageId,
data.map((x) => prepareCluster(x, context, true))
)
}
/**
* Prepare tag object from tag
* @param {*} tag
* @returns tag information
*/
function prepareTag(tag) {
return {
name: tag.$.name,
description: tag.$.description
}
}
/**
* Process defaultAccess tag in the XML.
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} defaultAccessList
*/
async function processDefaultAccess(
db,
filePath,
packageId,
defaultAccessList
) {
let p = []
for (const da of defaultAccessList) {
let type = {
type: da.$.type,
access: []
}
for (const ac of da.access) {
type.access.push(extractAccessTag(ac))
}
p.push(queryLoader.insertDefaultAccess(db, packageId, type))
}
return Promise.all(p)
}
/**
* Process accessControl tag in the XML.
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} accessControlList
*/
async function processAccessControl(
db,
filePath,
packageId,
accessControlList
) {
let operations = []
let roles = []
let accessModifiers = []
for (const ac of accessControlList) {
if ('operation' in ac) {
for (const op of ac.operation) {
operations.push({
name: op.$.type,
description: op.$.description
})
}
}
if ('role' in ac) {
for (const role of ac.role) {
roles.push({
name: role.$.type,
description: role.$.description,
level: roles.length
})
}
}
if ('privilege' in ac) {
for (const role of ac.privilege) {
roles.push({
name: role.$.type,
description: role.$.description,
level: roles.length
})
}
}
if ('modifier' in ac) {
for (const modifier of ac.modifier) {
accessModifiers.push({
name: modifier.$.type,
description: modifier.$.description
})
}
}
}
let p = []
p.push(queryLoader.insertAccessRoles(db, packageId, roles))
p.push(queryLoader.insertAccessOperations(db, packageId, operations))
p.push(queryLoader.insertAccessModifiers(db, packageId, accessModifiers))
return Promise.all(p)
}
/**
* Processes the tags in the XML.
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} tags
*/
async function processTags(db, filePath, packageId, tags) {
// <tag name="AB" description="Description"/>
env.logDebug(`${filePath}, ${packageId}: ${tags.length} tags.`)
let preparedTags = tags.map((x) => prepareTag(x))
return queryLoader.insertTags(db, packageId, preparedTags, null)
}
/**
* Convert domain from XMl to domain for DB.
*
* @param {*} domain
* @returns Domain object for DB.
*/
function prepareDomain(domain) {
let d = {
name: domain.$.name,
specCode: domain.$.spec,
specDescription: `Latest ${domain.$.name} spec: ${domain.$.spec}`,
specCertifiable: domain.$.certifiable == 'true'
}
if ('older' in domain) {
d.older = domain.older.map((old) => {
return {
specCode: old.$.spec,
specDescription: `Older ${domain.$.name} spec ${old.$.spec}`,
specCertifiable: old.$.certifiable == 'true'
}
})
}
return d
}
/**
* Process domains for insertion.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} data
* @returns Promise of database insertion of domains.
*/
async function processDomains(db, filePath, packageId, data) {
// <domain name="ZLL" spec="zll-1.0-11-0037-10" dependsOn="zcl-1.0-07-5123-03">
// <older ....
// </domain>
env.logDebug(`${filePath}, ${packageId}: ${data.length} domains.`)
let preparedDomains = data.map((x) => prepareDomain(x))
let preparedSpecs = preparedDomains.filter((d) => d.specCode != null)
let specIds = await queryLoader.insertSpecs(db, packageId, preparedSpecs)
for (let i = 0; i < specIds.length; i++) {
preparedDomains[i].specRef = specIds[i]
}
return queryLoader.insertDomains(db, packageId, preparedDomains)
}
/**
* Prepare Data Type Discriminator for database table insertion.
*
* @param {*} a
* @returns An Object
*/
function prepareDataTypeDiscriminator(a) {
return {
name: a.name,
id: a.id
}
}
/**
* Processes Data Type Discriminator.
*
* @param {*} db
* @param {*} filePath
* @param {*} zclDataTypes
* @returns Promise of inserted Data Type Discriminators.
*/
async function processDataTypeDiscriminator(db, packageId, zclDataTypes) {
// Loading the Data Types using ZCLDataTypes mentioned in zcl.json metadata
// file
let types = zclDataTypes.map((x, index) => {
return { id: index + 1, name: x }
})
env.logDebug(`${packageId}: ${types.length} Data Type Discriminator.`)
return queryLoader.insertDataTypeDiscriminator(
db,
packageId,
types.map((x) => prepareDataTypeDiscriminator(x))
)
}
/**
* Prepare Data Types for database table insertion.
*
* @param {*} a
* @param {*} dataType
* @param {*} typeMap
* @returns An Object
*/
function prepareDataType(a, dataType, typeMap) {
let dataTypeRef = 0
// The following is when the dataType is atomic
if (!dataType && a.$.name.toLowerCase().includes(dbEnum.zclType.bitmap)) {
dataTypeRef = typeMap.get(dbEnum.zclType.bitmap)
} else if (
!dataType &&
a.$.name.toLowerCase().includes(dbEnum.zclType.enum)
) {
dataTypeRef = typeMap.get(dbEnum.zclType.enum)
} else if (
!dataType &&
a.$.name.toLowerCase().includes(dbEnum.zclType.string)
) {
dataTypeRef = typeMap.get(dbEnum.zclType.string)
} else if (
!dataType &&
a.$.name.toLowerCase().includes(dbEnum.zclType.struct)
) {
dataTypeRef = typeMap.get(dbEnum.zclType.struct)
} else if (!dataType) {
dataTypeRef = typeMap.get(dbEnum.zclType.number)
}
return {
name: a.$.name,
id: parseInt(a.$.id),
description: a.$.description ? a.$.description : a.$.name,
discriminator_ref: dataType ? dataType : dataTypeRef,
cluster_code: a.cluster
? a.cluster
: a.$.cluster_code
? [{ $: { code: a.$.cluster_code[0] } }]
: null // else case: Treating features in a cluster as a bitmap
}
}
/**
* Processes Data Type.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @param {*} dataType
* @returns Promise of inserted Data Types into the Data Type table.
*/
async function processDataType(
db,
filePath,
packageId,
knownPackages,
data,
dataType
) {
let typeMap = await zclLoader.getDiscriminatorMap(db, knownPackages)
if (dataType == dbEnum.zclType.atomic) {
let types = data[0].type
env.logDebug(`${filePath}, ${packageId}: ${data.length} Atomic Data Types.`)
return queryLoader.insertDataType(
db,
packageId,
types.map((x) => prepareDataType(x, 0, typeMap))
)
} else if (dataType == dbEnum.zclType.enum) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} Enum Data Types.`)
return queryLoader.insertDataType(
db,
packageId,
data.map((x) =>
prepareDataType(x, typeMap.get(dbEnum.zclType.enum), typeMap)
)
)
} else if (dataType == dbEnum.zclType.bitmap) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} Bitmap Data Types.`)
return queryLoader.insertDataType(
db,
packageId,
data.map((x) =>
prepareDataType(x, typeMap.get(dbEnum.zclType.bitmap), typeMap)
)
)
} else if (dataType == dbEnum.zclType.struct) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} Struct Data Types.`)
return queryLoader.insertDataType(
db,
packageId,
data.map((x) =>
prepareDataType(x, typeMap.get(dbEnum.zclType.struct), typeMap)
)
)
} else if (dataType == dbEnum.zclType.string) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} String Data Types.`)
return queryLoader.insertDataType(
db,
packageId,
data.map((x) =>
prepareDataType(x, typeMap.get(dbEnum.zclType.string), typeMap)
)
)
} else {
env.logError(
'Could not find the discriminator for the data type: ' + dataType
)
queryPackageNotification.setNotification(
dnb,
'ERROR',
'Could not find the discriminator for the data type: ' + dataType,
packageId,
1
)
}
}
/**
* Prepare numbers for database table insertion.
*
* @param {*} a
* @param {*} dataType
* @returns An Object
*/
function prepareNumber(a, dataType) {
// Adding explicit exceptions for signed types when xml does not specify it
let isSignedException = false
if (
(!('signed' in a.$) && a.$.name.toLowerCase() == 'single') ||
a.$.name.toLowerCase() == 'double'
) {
isSignedException = true
}
return {
size: a.$.size,
is_signed:
'signed' in a.$
? a.$.signed.toLowerCase() === 'true'
? 1
: 0
: isSignedException || /^int[0-9]{1,2}s?$/.test(a.$.name)
? 1
: 0,
name: a.$.name,
cluster_code: a.cluster ? a.cluster : null,
discriminator_ref: dataType
}
}
/**
* Processes Numbers.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns Promise of inserted numbers into the number table.
*/
async function processNumber(db, filePath, packageId, knownPackages, data) {
let typeMap = await zclLoader.getDiscriminatorMap(db, knownPackages)
let numbers = data[0].type.filter(function (item) {
return (
!item.$.name.toLowerCase().includes(dbEnum.zclType.bitmap) &&
!item.$.name.toLowerCase().includes(dbEnum.zclType.enum) &&
!item.$.name.toLowerCase().includes(dbEnum.zclType.string) &&
!item.$.name.toLowerCase().includes(dbEnum.zclType.struct)
)
})
env.logDebug(`${filePath}, ${packageId}: ${data.length} Number Types.`)
return queryLoader.insertNumber(
db,
packageId,
numbers.map((x) => prepareNumber(x, typeMap.get(dbEnum.zclType.number)))
)
}
/**
* Prepare strings for database table insertion.
*
* @param {*} a
* @param {*} dataType
* @returns An Object
*/
function prepareString(a, dataType) {
return {
is_long: a.$.long && a.$.long.toLowerCase() == 'true' ? 1 : 0,
size: a.$.size,
is_char: 0,
name: a.$.name,
cluster_code: a.cluster ? a.cluster : null,
discriminator_ref: dataType
}
}
/**
* Processes Strings.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns Promise of inserted strings into the String table.
*/
async function processString(db, filePath, packageId, knownPackages, data) {
let typeMap = await zclLoader.getDiscriminatorMap(db, knownPackages)
let strings = data[0].type.filter(function (item) {
return (
(item.$.string && item.$.string.toLowerCase() == 'true') ||
(item.$.name && item.$.name.toLowerCase().includes('string'))
)
})
env.logDebug(`${filePath}, ${packageId}: ${data.length} String Types.`)
return queryLoader.insertString(
db,
packageId,
strings.map((x) => prepareString(x, typeMap.get(dbEnum.zclType.string)))
)
}
/**
* Prepare enums or bitmaps for database table insertion.
*
* @param {*} a
* @param {*} dataType
* @returns An Object
*/
function prepareEnumOrBitmapAtomic(a, dataType) {
return {
size: a.$.size,
name: a.$.name,
cluster_code: a.cluster ? a.cluster : null,
discriminator_ref: dataType
}
}
/**
* Processes the enums.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns A promise of inserted enums.
*/
async function processEnumAtomic(db, filePath, packageId, knownPackages, data) {
let typeMap = await zclLoader.getDiscriminatorMap(db, knownPackages)
let enums = data[0].type.filter(function (item) {
return item.$.name.toLowerCase().includes('enum')
})
env.logDebug(`${filePath}, ${packageId}: ${data.length} Baseline Enum Types.`)
return queryLoader.insertEnumAtomic(
db,
packageId,
enums.map((x) =>
prepareEnumOrBitmapAtomic(x, typeMap.get(dbEnum.zclType.enum))
)
)
}
/**
* Prepare enums or bitmaps for database table insertion.
*
* @param {*} a
* @param {*} dataType
* @returns An Object
*/
function prepareEnumOrBitmap(db, packageId, a, dataType, typeMap) {
// Taking care of a typo for backwards compatibility
// for eg <enum name="Status" type="INT8U" i.e. an enum defined as int8u
let enumIndex = typeMap.get(dbEnum.zclType.enum)
if (
dataType == enumIndex &&
(a.$.type.toLowerCase().includes('int') ||
a.$.type.toLowerCase().includes(dbEnum.zclType.bitmap))
) {
let message =
'Check type contradiction in XML metadata for ' +
a.$.name +
' with type ' +
a.$.type
env.logWarning(message)
queryPackageNotification.setNotification(
db,
'WARNING',
message,
packageId,
2
)
a.$.type = 'enum' + a.$.type.toLowerCase().match(/\d+/g).join('')
}
return {
name: a.$.name,
type: a.$.type.toLowerCase(),
cluster_code: a.cluster
? a.cluster
: a.$.cluster_code
? [{ $: { code: a.$.cluster_code[0] } }]
: null, // else case: Treating features in a cluster as a bitmap
discriminator_ref: dataType,
apiMaturity: a.$.apiMaturity
}
}
/**
* Processes the enums.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns A promise of inserted enums.
*/
async function processEnum(db, filePath, packageId, knownPackages, data) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} Enum Types.`)
let typeMap = await zclLoader.getDiscriminatorMap(db, knownPackages)
return queryLoader.insertEnum(
db,
knownPackages,
data.map((x) =>
prepareEnumOrBitmap(
db,
packageId,
x,
typeMap.get(dbEnum.zclType.enum),
typeMap
)
)
)
}
/**
* Processes the enum Items.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns A promise of inserted enum items.
*/
async function processEnumItems(db, filePath, packageId, knownPackages, data) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} Enum Items.`)
let enumItems = []
let lastFieldId = -1
data.forEach((e) => {
if ('item' in e) {
e.item.forEach((item) => {
let defaultFieldId = lastFieldId + 1
lastFieldId = item.$.fieldId ? parseInt(item.$.fieldId) : defaultFieldId
enumItems.push({
enumName: e.$.name,
enumClusterCode: e.cluster ? e.cluster : null,
name: item.$.name,
value: parseInt(item.$.value),
fieldIdentifier: lastFieldId,
apiMaturity: item.$.apiMaturity
})
})
}
})
return queryLoader.insertEnumItems(db, packageId, knownPackages, enumItems)
}
/**
* Processes the bitmaps.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns A promise of inserted bitmaps.
*/
async function processBitmapAtomic(
db,
filePath,
packageId,
knownPackages,
data
) {
let typeMap = await zclLoader.getDiscriminatorMap(db, knownPackages)
let bitmaps = data[0].type.filter(function (item) {
return item.$.name.toLowerCase().includes(dbEnum.zclType.bitmap)
})
env.logDebug(
`${filePath}, ${packageId}: ${data.length} Baseline Bitmap Types.`
)
return queryLoader.insertBitmapAtomic(
db,
packageId,
bitmaps.map((x) =>
prepareEnumOrBitmapAtomic(x, typeMap.get(dbEnum.zclType.bitmap))
)
)
}
/**
* Processes the bitmaps.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns A promise of inserted bitmaps.
*/
async function processBitmap(db, filePath, packageId, knownPackages, data) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} Bitmap Types.`)
let typeMap = await zclLoader.getDiscriminatorMap(db, knownPackages)
return queryLoader.insertBitmap(
db,
knownPackages,
data.map((x) =>
prepareEnumOrBitmap(
db,
packageId,
x,
typeMap.get(dbEnum.zclType.bitmap),
typeMap
)
)
)
}
/**
* Processes the bitmap fields.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns A promise of inserted bitmap fields.
*/
async function processBitmapFields(
db,
filePath,
packageId,
knownPackages,
data
) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} Bitmap Fields.`)
let bitmapFields = []
let lastFieldId = -1
if (!('features' in data)) {
data.forEach((bm) => {
if ('field' in bm) {
bm.field.forEach((item) => {
let defaultFieldId = lastFieldId + 1
lastFieldId = item.$.fieldId
? parseInt(item.$.fieldId)
: defaultFieldId
bitmapFields.push({
bitmapName: bm.$.name,
bitmapClusterCode: bm.cluster ? bm.cluster : null,
name: item.$.name,
mask: parseInt(item.$.mask),
fieldIdentifier: lastFieldId,
apiMaturity: item.$.apiMaturity
})
})
}
})
// Treating features in a cluster as a bitmap
} else if (
'features' in data &&
data.features.length == 1 &&
'feature' in data.features[0]
) {
let clusterCodes = [{ $: { code: data.code[0] } }]
if ('cluster' in data.features[0] && data.features[0].cluster.length > 0) {
data.features[0].cluster.forEach((clCode) => {
if (
!clusterCodes.some(
(existingCode) => existingCode.$.code === clCode.$.code
)
) {
clusterCodes.push({ $: { code: clCode.$.code } })
}
})
}
clusterCodes.forEach((clCode) =>
data.features[0].feature.forEach((item) => {
let itemBit = item.$.bit
if (isNaN(itemBit)) {
throw new Error(`Invalid bit value: ${itemBit}`)
}
bitmapFields.push({
bitmapName: 'Feature',
bitmapClusterCode: [clCode],
name: item.$.name,
mask: 1 << itemBit,
fieldIdentifier: itemBit,
apiMaturity: item.$.apiMaturity
})
})
)
}
return queryLoader.insertBitmapFields(
db,
packageId,
knownPackages,
bitmapFields
)
}
/**
* Prepare structs for database table insertion.
*
* @param {*} a
* @param {*} dataType
* @returns An Object
*/
function prepareStruct(a, dataType) {
return {
name: a.$.name,
cluster_code: a.cluster ? a.cluster : null,
discriminator_ref: dataType,
isFabricScoped: a.$.isFabricScoped == 'true',
apiMaturity: a.$.apiMaturity
}
}
/**
* Processes the structs.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} data
* @returns A promise of inserted structs.
*/
async function processStruct(db, filePath, packageId, knownPackages, data) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} Struct Types.`)
let typeMap = await zclLoader.getDiscriminatorMap(db, knownPackages)
return queryLoader.insertStruct(
db,
knownPackages,
data.map((x) => prepareStruct(x, typeMap.get(dbEnum.zclType.struct)))
)
}
/**
* Processes the struct Items.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageIds
* @param {*} data
* @returns A promise of inserted struct items.
*/
async function processStructItems(db, filePath, packageIds, data, context) {
env.logDebug(`${filePath}, ${packageIds}: ${data.length} Struct Items.`)
let structItems = []
data.forEach((si) => {
let lastFieldId = -1
if ('item' in si) {
si.item.forEach((item) => {
let defaultFieldId = lastFieldId + 1
lastFieldId = item.$.fieldId ? parseInt(item.$.fieldId) : defaultFieldId
structItems.push({
structName: si.$.name,
structClusterCode: si.cluster ? si.cluster : null,
name: item.$.name,
type:
item.$.type == item.$.type.toUpperCase() && item.$.type.length > 1
? item.$.type.toLowerCase()
: item.$.type,
fieldIdentifier: lastFieldId,
minLength: 0,
maxLength: item.$.length ? item.$.length : null,
defaultValue: item.$.default ? item.$.default : null,
isWritable: item.$.writable == 'true',
isArray: item.$.array == 'true' ? true : false,
isEnum: item.$.enum == 'true' ? true : false,
isNullable: item.$.isNullable == 'true' ? true : false,
isOptional: item.$.optional == 'true' ? true : false,
isFabricSensitive: item.$.isFabricSensitive == 'true' ? true : false,
apiMaturity: item.$.apiMaturity
})
})
}
if (
context.fabricHandling &&
context.fabricHandling.automaticallyCreateFields &&
si.$.isFabricScoped == 'true'
) {
structItems.push({
structName: si.$.name,
structClusterCode: si.cluster ? si.cluster : null,
name: context.fabricHandling.indexFieldName,
type: context.fabricHandling.indexType,
fieldIdentifier: context.fabricHandling.indexFieldId,
minLength: 0,
maxLength: null,
isWritable: false,
isArray: false,
isEnum: false,
isNullable: false,
isOptional: false,
isFabricSensitive: false,
apiMaturity: null
})
}
})
return queryLoader.insertStructItems(db, packageIds, structItems)
}
/**
* Prepares a device type object by extracting and transforming its properties.
*
* This function takes a device type object and processes its properties to create
* a new object with a specific structure. It handles various properties such as
* device ID, profile ID, domain, name, description, class, scope, and superset.
* Additionally, it processes endpoint compositions and clusters if they exist.
*
* @param {Object} deviceType - The device type object to be prepared.
* @returns {Object} The prepared device type object with transformed properties.
*/
function prepareDeviceType(deviceType) {
let ret = {
code: parseInt(deviceType.deviceId[0]['_']),
profileId: parseInt(deviceType.profileId[0]['_']),
domain: deviceType.domain[0],
name: deviceType.name[0],
description: deviceType.typeName[0],
class: deviceType.class ? deviceType.class[0] : '',
scope: deviceType.scope ? deviceType.scope[0] : '',
superset: deviceType.superset ? deviceType.superset[0] : '',
revision: deviceType.revision ? parseInt(deviceType.revision[0]['_']) : 1,
compositionType: null
}
if ('endpointComposition' in deviceType) {
try {
ret.compositionType = deviceType.endpointComposition[0].compositionType[0]
ret.composition = deviceType.endpointComposition[0]
} catch (error) {
console.error('Error processing endpoint composition:', error)
}
}
if ('clusters' in deviceType) {
ret.clusters = []
deviceType.clusters.forEach((cluster) => {
if ('include' in cluster) {
cluster.include.forEach((include) => {
let attributes = []
let commands = []
let features = []
if ('requireAttribute' in include) {
attributes = include.requireAttribute
}
if ('requireCommand' in include) {
commands = include.requireCommand
}
if ('features' in include) {
include.features[0].feature.forEach((f) => {
features.push({
code: f.$.code,
conformance: conformParser.parseConformanceFromXML(f)
})
})
}
ret.clusters.push({
client: 'true' == include.$.client,
server: 'true' == include.$.server,
clientLocked: 'true' == include.$.clientLocked,
serverLocked: 'true' == include.$.serverLocked,
clusterName:
include.$.cluster != undefined ? include.$.cluster : include._,
requiredAttributes: attributes,
requiredCommands: commands,
features: features
})
})
}
})
}
return ret
}
/**
* Processes and inserts device types into the database.
* This function logs the number of device types being processed for debugging purposes.
* It maps over the provided data to prepare each device type and then iterates over each prepared device type.
* If a device type has a compositionType, it inserts the endpoint composition into the database,
* retrieves the endpoint composition ID, and then inserts the device composition.
* Finally, it inserts all prepared device types into the database.
*
* @param {*} db - The database connection object.
* @param {string} filePath - The file path from which the device types are being processed.
* @param {*} packageId - The package ID associated with the device types.
* @param {Array} data - The array of device types to be processed.
* @param {*} context - Additional context that might be required for processing.
* @returns {Promise} A promise that resolves after all device types have been inserted into the database.
*/
async function processDeviceTypes(db, filePath, packageId, data, context) {
env.logDebug(`${filePath}, ${packageId}: ${data.length} deviceTypes.`)
let deviceTypes = data.map((x) => prepareDeviceType(x))
for (let deviceType of deviceTypes) {
if (
deviceType.compositionType != null ||
deviceType.code == parseInt(context.mandatoryDeviceTypes, 16)
) {
await queryLoader.insertEndpointComposition(db, deviceType, context)
if (deviceType.code !== parseInt(context.mandatoryDeviceTypes, 16)) {
let endpointCompositionId =
await queryLoader.getEndpointCompositionIdByCode(db, deviceType)
await queryLoader.insertDeviceComposition(
db,
deviceType,
endpointCompositionId
)
}
}
}
return queryLoader.insertDeviceTypes(db, packageId, deviceTypes)
}
/**
* Processes and reloads device type entities in the database.
* This function is called when a custom xml with device types is reloaded.
*
* @returns {Promise} A promise that resolves after all device types have been reloaded.
*/
async function processReloadDeviceTypes(db, packageId, data, sessionPackages) {
let deviceTypes = data.map((x) => prepareDeviceType(x))
return queryLoader.reloadDeviceTypes(
db,
packageId,
deviceTypes,
sessionPackages
)
}
/**
* Process promises for loading the data types
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} toplevel
* @returns Promise of data types
*/
async function processDataTypes(
db,
filePath,
packageId,
knownPackages,
toplevel,
featureClusters
) {
let dataTypePromises = []
if (dbEnum.zclType.atomic in toplevel) {
dataTypePromises.push(
processDataType(
db,
filePath,
packageId,
knownPackages,
toplevel.atomic,
dbEnum.zclType.atomic
)
)
}
if (dbEnum.zclType.bitmap in toplevel) {
dataTypePromises.push(
processDataType(
db,
filePath,
packageId,
knownPackages,
toplevel.bitmap,
dbEnum.zclType.bitmap
)
)
}
// Treating features in a cluster as a bitmap
if (featureClusters.length > 0) {
featureClusters.forEach((fc) => {
let featureClusterCodes = [fc.code[0]]
if ('cluster' in fc.features[0] && fc.features[0].cluster.length > 0) {
fc.features[0].cluster.forEach((clCode) => {
if (!featureClusterCodes.includes(clCode.$.code)) {
featureClusterCodes.push(clCode.$.code)
}
})
}
featureClusterCodes.forEach((fcCode) => {
dataTypePromises.push(
processDataType(
db,
filePath,
packageId,
knownPackages,
[
{
$: {
name: 'Feature',
type: 'BITMAP32',
cluster_code: [fcCode]
}
}
],
dbEnum.zclType.bitmap
)
)
})
})
}
if (dbEnum.zclType.enum in toplevel) {
dataTypePromises.push(
processDataType(
db,
filePath,
packageId,
knownPackages,
toplevel.enum,
dbEnum.zclType.enum
)
)
}
if (dbEnum.zclType.struct in toplevel) {
dataTypePromises.push(
processDataType(
db,
filePath,
packageId,
knownPackages,
toplevel.struct,
dbEnum.zclType.struct
)
)
}
return Promise.all(dataTypePromises)
}
/**
* Processes promises for loading individual tables per data type for
* atomics/baseline types.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} toplevel
* @returns Promise of atomic/baseline processing.
*/
async function processAtomicTypes(
db,
filePath,
packageId,
knownPackages,
toplevel
) {
let atomicPromises = []
if (dbEnum.zclType.atomic in toplevel) {
atomicPromises.push(
processNumber(db, filePath, packageId, knownPackages, toplevel.atomic)
)
atomicPromises.push(
processString(db, filePath, packageId, knownPackages, toplevel.atomic)
)
atomicPromises.push(
processEnumAtomic(db, filePath, packageId, knownPackages, toplevel.atomic)
)
atomicPromises.push(
processBitmapAtomic(
db,
filePath,
packageId,
knownPackages,
toplevel.atomic
)
)
}
return Promise.all(atomicPromises)
}
/**
* Processes promises for loading individual tables per data type for no-atomic
* and inherited types.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} toplevel
* @param {*} featureClusters
* @returns Promise of non-atomic/inherited data type processing.
*/
async function processNonAtomicTypes(
db,
filePath,
packageId,
knownPackages,
toplevel,
featureClusters
) {
let nonAtomicPromises = []
if (dbEnum.zclType.enum in toplevel) {
nonAtomicPromises.push(
processEnum(db, filePath, packageId, knownPackages, toplevel.enum)
)
}
if (dbEnum.zclType.bitmap in toplevel) {
nonAtomicPromises.push(
processBitmap(db, filePath, packageId, knownPackages, toplevel.bitmap)
)
}
// Treating features in a cluster as a bitmap
if (featureClusters.length > 0) {
featureClusters.forEach((fc) => {
let featureClusterCodes = [fc.code[0]]
if ('cluster' in fc.features[0] && fc.features[0].cluster.length > 0) {
fc.features[0].cluster.forEach((clCode) => {
if (!featureClusterCodes.includes(clCode.$.code)) {
featureClusterCodes.push(clCode.$.code)
}
})
}
featureClusterCodes.forEach((fcCode) => {
nonAtomicPromises.push(
processBitmap(db, filePath, packageId, knownPackages, [
{
$: {
name: 'Feature',
type: 'BITMAP32',
cluster_code: [fcCode]
}
}
])
)
})
})
}
if (dbEnum.zclType.struct in toplevel) {
nonAtomicPromises.push(
processStruct(db, filePath, packageId, knownPackages, toplevel.struct)
)
}
return Promise.all(nonAtomicPromises)
}
/**
* Processes promises for loading items within a bitmap, struct, and enum data types.
*
* @param {*} db
* @param {*} filePath
* @param {*} packageId
* @param {*} knownPackages
* @param {*} toplevel
* @param {*} featureClusters
* @param {*} context
* @param {*} collectedStructItems
* @returns Promise of processing sub items within a bitmap, enum and structs.
*/
async function processSubItems(
db,
filePath,
packageId,
knownPackages,
toplevel,
featureClusters,
context,
collectedStructItems
) {
let subItemPromises = []
if (dbEnum.zclType.enum in toplevel) {
subItemPromises.push(
processEnumItems(db, filePath, packageId, knownPackages, toplevel.enum)
)
}
if (dbEnum.zclType.bitmap in toplevel) {
subItemPromises.push(
processBitmapFields(
db,
filePath,
packageId,
knownPackages,
toplevel.bitmap
)
)
}
// Treating features in a cluster as a bitmap
if (featureClusters && featureClusters.length > 0) {
featureClusters.forEach((fc) => {
subItemPromises.push(
processBitmapFields(db, filePath, packageId, knownPackages, fc)
)
})
}
// Delaying the loading of struct items into collectedStructItems instead of
// processing them with other subitems. This is because the struct items are
// dependent on types which could span across multiple xml files.
if (dbEnum.zclType.struct in toplevel) {
collectedStructItems.push([
db,
filePath,
knownPackages,
toplevel.struct,
context
])
}
return Promise.all(subItemPromises)
}
/**
* After XML parser is done with the barebones parsing, this function
* branches the individual toplevel tags.
*
* @param {*} db
* @param {*} argument
* @param {*} previouslyKnownPackages
* @param {*} context
* @param {*} collectedStructItems
* @returns promise that resolves when all the subtags are parsed.
*/
async function processParsedZclData(
db,
argument,
previouslyKnownPackages,
context,
collectedStructItems
) {
let filePath = argument.filePath
let data = argument.result
let packageId = argument.packageId
previouslyKnownPackages.add(packageId)
let knownPackages = Array.from(previouslyKnownPackages)
let featureClusters = []
if (!('result' in argument)) {
return []
} else {
let toplevel = null
if ('configurator' in data) {
toplevel = data.configurator
}
if ('zap' in data) {
toplevel = data.zap
}
if (toplevel == null) return []
// We load in multiple batches, since each batch needs to resolve
// before the next batch can be loaded, as later data depends on
// previous data. Final batch is delayed, meaning that
// the promises there can't start yet, until all files are loaded.
// Batch 1: load accessControl, tag and domain
let batch1 = []
if ('accessControl' in toplevel) {
batch1.push(
processAccessControl(db, filePath, packageId, toplevel.accessControl)
)
}
if ('tag' in toplevel) {
batch1.push(processTags(db, filePath, packageId, toplevel.tag))
}
if ('domain' in toplevel) {
batch1.push(processDomains(db, filePath, packageId, toplevel.domain))
}
await Promise.all(batch1)
// Batch 2: device types, globals, clusters
let batch2 = []
if ('deviceType' in toplevel) {
batch2.push(
processDeviceTypes(
db,
filePath,
packageId,
toplevel.deviceType,
context
)
)
}
if ('global' in toplevel) {
batch2.push(
processGlobals(db, filePath, packageId, toplevel.global, context)
)
}
if ('cluster' in toplevel) {
featureClusters = toplevel.cluster.filter((c) => 'features' in c)
batch2.push(
processClusters(db, filePath, packageId, toplevel.cluster, context)
)
}
await Promise.all(batch2)
// Batch 3: Load the data type table which lists all data types
await processDataTypes(
db,
filePath,
packageId,
knownPackages,
toplevel,
featureClusters
)
// Batch4 and Batch5: Loads the inidividual tables per data type from
// atomics/baseline types to inherited types
await processAtomicTypes(db, filePath, packageId, knownPackages, toplevel)
await processNonAtomicTypes(
db,
filePath,
packageId,
knownPackages,
toplevel,
featureClusters
)
// Batch6: Loads the items within a bitmap, struct and enum data types
await processSubItems(
db,
filePath,
packageId,
knownPackages,
toplevel,
featureClusters,
context,
collectedStructItems
)
// Batch7: Loads the defaultAccess
let Batch7 = []
if ('defaultAccess' in toplevel) {
Batch7.push(
processDefaultAccess(db, filePath, packageId, toplevel.defaultAccess)
)
}
if ('atomic' in toplevel) {
Batch7.push(processAtomics(db, filePath, packageId, toplevel.atomic))
}
await Promise.all(Batch7)
//}
// Batch 8: cluster extensions and global attributes
// These don't start right away, but are delayed. So we don't return
// promises that have already started, but functions that return promises.
let delayedPromises = []
if ('cluster' in toplevel) {
delayedPromises.push(() =>
processClusterGlobalAttributes(
db,
filePath,
packageId,
toplevel.cluster
)
)
}
if ('clusterExtension' in toplevel) {
delayedPromises.push(() =>
processClusterExtensions(
db,
filePath,
packageId,
knownPackages,
toplevel.clusterExtension,
context
)
)
}
return Promise.all(delayedPromises)
}
}
/**
* This function is used for parsing each individual ZCL file at a grouped zcl file package level.
* This should _not_ be used for custom XML addition due to custom xmls potentially relying on existing packges.
* @param {*} db
* @param {*} packageId
* @param {*} file
* @param {*} context
* @param {*} collectedStructItems
* @returns A promise for when the last stage of the loading pipeline finishes.
*/
async function parseSingleZclFile(
db,
packageId,
file,
context,
collectedStructItems
) {
try {
let fileContent = await fsp.readFile(file)
let data = {
filePath: file,
data: fileContent,
crc: util.checksum(fileContent)
}
let result = await zclLoader.qualifyZclFile(
db,
data,
packageId,
dbEnum.packageType.zclXml,
false
)
if (result.data) {
result.result = await util.parseXml(fileContent)
delete result.data
}
return processParsedZclData(
db,
result,
new Set(),
context,
collectedStructItems
)
} catch (err) {
err.message = `Error reading xml file: ${file}\n` + err.message
throw err
}
}
/**
* Checks if there is a crc mismatch on any xml file. This can be used to
* decide if there is a need to reload all the xml files. Also check if the
* package is not loaded before.
* @param {*} db
* @param {*} packageId
* @param {*} files
* @returns the status of crc mismatch and whether a package is present in an
* object
*/
async function isCrcMismatchOrPackageDoesNotExist(db, packageId, files) {
let packagesNotFound = 0
let packagesFound = 0
let result = { isCrcMismatch: false, areSomePackagesNotLoaded: false }
for (let file of files) {
let fileContent = await fsp.readFile(file)
let filePath = file
let actualCrc = util.checksum(fileContent)
let pkg = await queryPackage.getPackageByPathAndParent(
db,
filePath,
packageId,
false
)
if (pkg != null && pkg.crc != actualCrc) {
env.logDebug(
`CRC missmatch for file ${pkg.path}, (${pkg.crc} vs ${actualCrc}) package id ${pkg.id}, parsing.
Mismatch with package id: ${packageId}`
)
result.isCrcMismatch = true
return result
} else if (pkg == null) {
// This is executed if there is no CRC in the database.
packagesNotFound++
env.logDebug(
`No CRC in the database for file ${filePath}. Package needs to be loaded`
)
} else if (pkg != null && pkg.crc == actualCrc) {
packagesFound++
}
}
result.areSomePackagesNotLoaded = !(
packagesNotFound == files.length || packagesFound == files.length
)
return result
}
/**
*
* Promises to iterate over all the XML files and returns an aggregate promise
* that will be resolved when all the XML files are done, or rejected if at least one fails.
*
* @param {*} db
* @param {*} packageId
* @param {*} zclFiles
* @param {*} context
* @returns Promise that resolves when all the individual promises of each file pass.
*/
async function parseZclFiles(db, packageId, zclFiles, context) {
env.logDebug(`Starting to parse ZCL files: ${zclFiles}`)
// Struct Items are part of delayed loading because they could have types
// belonging to a different file.
let collectedStructItems = []
// Populate the Data Type Discriminator
if (context.ZCLDataTypes)
await processDataTypeDiscriminator(db, packageId, context.ZCLDataTypes)
// Load the Types File first such the atomic types are loaded and can be
// referenced by other types
let typesFiles = zclFiles.filter((file) => file.includes('types.xml'))
let typeFilePromise = typesFiles.map((file) =>
parseSingleZclFile(db, packageId, file, context, collectedStructItems)
)
await Promise.all(typeFilePromise)
// Load everything apart from atomic data types
let nonTypesFiles = zclFiles.filter((file) => !file.includes('types.xml'))
let individualFilePromise = nonTypesFiles.map((file) =>
parseSingleZclFile(db, packageId, file, context, collectedStructItems)
)
let individualResults = await Promise.all(individualFilePromise)
let laterPromises = individualResults.flat(2)
await Promise.all(laterPromises.map((promise) => promise()))
// Process collected struct items now because data types from all files have been loaded.
if (collectedStructItems.length > 0) {
let processStructItemsPromises = collectedStructItems.map((args) =>
processStructItems(...args)
)
await Promise.all(processStructItemsPromises)
}
// Load some missing content which was not possible before the above was done
return zclLoader.processZclPostLoading(db, packageId)
}
/**
* Parses the manufacturers xml.
*
* @param {*} db
* @param {*} ctx
* @returns Promise of a parsed manufacturers file.
*/
async function parseManufacturerData(db, packageId, manufacturersXml) {
let data = await fsp.readFile(manufacturersXml)
let manufacturerMap = await util.parseXml(data)
return queryPackage.insertOptionsKeyValues(
db,
packageId,
dbEnum.packageOptionCategory.manufacturerCodes,
manufacturerMap.map.mapping.map((datum) => {
let mfgPair = datum['$']
return { code: mfgPair['code'], label: mfgPair['translation'] }
})
)
}
/**
* Parses the profiles xml.
*
* @param {*} db
* @param {*} ctx
* @returns Promise of a parsed profiles file.
*/
async function parseProfilesData(db, packageId, profilesXml) {
let data = await fsp.readFile(profilesXml)
let profilesMap = await util.parseXml(data)
return queryPackage.insertOptionsKeyValues(
db,
packageId,
dbEnum.packageOptionCategory.profileCodes,
profilesMap.map.mapping.map((datum) => {
let profilePair = datum['$']
return { code: profilePair['code'], label: profilePair['translation'] }
})
)
}
/**
* Inside the `zcl.json` can be a `featureFlags` key, which is
* a general purpose object. It contains keys, that map to objects.
* Each key is a "package option category".
* Key/velues of the object itself, end up in CODE/LABEL combinations.
*
* @param {*} db
* @param {*} packageId
* @param {*} featureFlags
* @returns array of feature flags
*/
async function parseFeatureFlags(db, packageId, featureFlags) {
return Promise.all(
Object.keys(featureFlags).map((featureCategory) => {
return queryPackage.insertOptionsKeyValues(
db,
packageId,
featureCategory,
Object.keys(featureFlags[featureCategory]).map((data) => {
return {
code: data,
label: featureFlags[featureCategory][data] == '1' ? true : false
}
})
)
})
)
}
/**
* Inside the `zcl.json` can be a `featureFlags` key, which is
* a general purpose object. It contains keys, that map to objects.
* Each key is a "package option category".
* Key/velues of the object itself, end up in CODE/LABEL combinations.
*
* @param {*} db
* @param {*} packageId
* @param {*} featureFlags
* @returns Promise that loads the uiOptions object into the database.
*/
async function parseUiOptions(db, packageId, uiOptions) {
let data = []
Object.keys(uiOptions).forEach((key) => {
data.push({
code: key,
label: uiOptions[key]
})
})
return queryPackage.insertOptionsKeyValues(
db,
packageId,
dbEnum.packageOptionCategory.ui,
data
)
}
/**
* Parses and loads the text and boolean options.
*
* @param {*} db
* @returns promise of parsed options
*/
async function parseOptions(db, packageId, options) {
let promises = []
promises.push(parseTextOptions(db, packageId, options.text))
promises.push(parseBoolOptions(db, packageId, options.bool))
return Promise.all(promises)
}
/**
* Parses the text options.
*
* @param {*} db
* @param {*} pkgRef
* @param {*} textOptions
* @returns Promise of a parsed text options.
*/
async function parseTextOptions(db, pkgRef, textOptions) {
if (!textOptions) return Promise.resolve()
let promises = Object.keys(textOptions).map((optionKey) => {
let val = textOptions[optionKey]
let optionValues
if (Array.isArray(val)) {
optionValues = val
} else {
optionValues = val.split(',').map((opt) => opt.trim())
}
return queryPackage.insertOptionsKeyValues(
db,
pkgRef,
optionKey,
optionValues.map((optionValue) => {
return { code: optionValue.toLowerCase(), label: optionValue }
})
)
})
return Promise.all(promises)
}
/**
* Parses the boolean options.
*
* @param {*} db
* @param {*} pkgRef
* @param {*} booleanCategories
* @returns Promise of a parsed boolean options.
*/
async function parseBoolOptions(db, pkgRef, booleanCategories) {
if (!booleanCategories) return Promise.resolve()
let options
if (Array.isArray(booleanCategories)) {
options = booleanCategories
} else {
options = booleanCategories
.split(',')
.map((optionValue) => optionValue.trim())
}
let promises = []
options.forEach((optionCategory) => {
promises.push(
queryPackage.insertOptionsKeyValues(db, pkgRef, optionCategory, [
{ code: 1, label: 'True' },
{ code: 0, label: 'False' }
])
)
})
return Promise.all(promises)
}
/**
* Asynchronously parses and inserts attribute access interface attributes into the database.
* This function iterates over the attributeAccessInterfaceAttributes object, processing each cluster
* by mapping its values to a specific structure and then inserting them into the database using
* the insertOptionsKeyValues function.
*
* The main purpose of this function is to store cluster/attribute pairs including global attributes and their cluster pair
* The ATTRIBUTE table has cluster_ref as null for global attributes so this second method was necessary
*
* @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.
* @returns {Promise<void>} A promise that resolves when all attributes have been processed and inserted.
*/
async function parseattributeAccessInterfaceAttributes(
db,
pkgRef,
attributeAccessInterfaceAttributes
) {
const clusters = Object.keys(attributeAccessInterfaceAttributes)
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
}))
// Insert the data into the database
try {
await queryPackage.insertOptionsKeyValues(
db,
pkgRef,
cluster,
optionsKeyValues
)
} catch (error) {
console.error(`Error inserting attributes for cluster ${cluster}:`, error)
}
}
}
/**
* Parses the default values inside the options.
*
* @param {*} db
* @param {*} ctx
* @returns Promised of parsed text and bool defaults.
*/
async function parseDefaults(db, packageId, defaults) {
let promises = []
promises.push(parseTextDefaults(db, packageId, defaults.text))
promises.push(parseBoolDefaults(db, packageId, defaults.bool))
return Promise.all(promises)
}
/**
* Parse text defaults from default options.
* @param {*} db
* @param {*} pkgRef
* @param {*} textDefaults
* @returns Array of promises
*/
async function parseTextDefaults(db, pkgRef, textDefaults) {
if (!textDefaults) return Promise.resolve()
let promises = []
for (let optionCategory of Object.keys(textDefaults)) {
let txt = textDefaults[optionCategory]
promises.push(
queryPackage
.selectSpecificOptionValue(db, pkgRef, optionCategory, txt)
.then((specificValue) => {
if (specificValue != null) return specificValue
if (_.isNumber(txt)) {
// Try to convert to hex.
let hex = '0x' + txt.toString(16)
return queryPackage.selectSpecificOptionValue(
db,
pkgRef,
optionCategory,
hex
)
} else {
return specificValue
}
})
.then((specificValue) => {
if (specificValue == null) {
env.logWarning(
'Default value for: ${optionCategory}/${txt} does not match an option for packageId: ' +
pkgRef
)
} else {
return queryPackage.insertDefaultOptionValue(
db,
pkgRef,
optionCategory,
specificValue.id
)
}
})
)
}
return Promise.all(promises)
}
/**
* Parse the boolean defaults inside options.
* @param {*} db
* @param {*} pkgRef
* @param {*} booleanCategories
* @returns List of promises
*/
async function parseBoolDefaults(db, pkgRef, booleanCategories) {
if (!booleanCategories) return Promise.resolve()
let promises = []
for (let optionCategory of Object.keys(booleanCategories)) {
promises.push(
queryPackage
.selectSpecificOptionValue(
db,
pkgRef,
optionCategory,
booleanCategories[optionCategory] ? 1 : 0
)
.then((specificValue) =>
queryPackage.insertDefaultOptionValue(
db,
pkgRef,
optionCategory,
specificValue.id
)
)
)
}
return Promise.all(promises)
}
/**
* Parses a single file. This function is used specifically
* for adding a package through an existing ZAP session because of its reliance
* on relating the new XML content to the packages associated with that session.
* e.g. for ClusterExtensions.
*
* @param {*} db
* @param {*} filePath
* @returns Promise of a loaded file.
*/
async function loadIndividualSilabsFile(db, filePath, sessionId) {
try {
let resolvedPath = await fsp.realpath(filePath)
if (path.extname(resolvedPath).toLowerCase() !== '.xml') {
let err = new Error(
`Unable to read file: ${resolvedPath}. Expecting an XML file with ZCL clusters.`
)
env.logWarning(err)
querySessionNotification.setNotification(
db,
'WARNING',
err,
sessionId,
2,
0
)
}
let fileContent = await fsp.readFile(resolvedPath)
let data = {
filePath: filePath,
data: fileContent,
crc: util.checksum(fileContent)
}
let result = await zclLoader.qualifyZclFile(
db,
data,
null,
dbEnum.packageType.zclXmlStandalone,
true
)
let pkgId = result.packageId
if (result.data) {
result.result = await util.parseXml(result.data)
delete result.data
if (
result.customXmlReload &&
result.result.configurator &&
(result.result.configurator.clusterExtension ||
result.result.configurator.deviceType)
) {
// If custom xml has device types, reload them so the device type entities are correctly linked
if (result.result.configurator.deviceType) {
let sessionPackages = await queryPackage.getSessionZclPackages(
db,
sessionId
)
let knownPackages = sessionPackages
.filter((pkg) =>
[
dbEnum.packageType.zclProperties,
dbEnum.packageType.zclXmlStandalone
].includes(pkg.type)
)
.map((pkg) => pkg.packageRef)
await processReloadDeviceTypes(
db,
pkgId,
result.result.configurator.deviceType,
knownPackages
)
}
// Reload cluster extension to link it to the correct top level package (if it exists)
if (result.result.configurator.clusterExtension) {
result.result = {
configurator: {
clusterExtension: result.result.configurator.clusterExtension
}
}
} else {
env.logDebug(
`CRC match for file ${result.filePath} (${result.crc}), skipping parsing.`
)
delete result.result
}
} else if (
result.customXmlReload &&
result.result.configurator &&
!result.result.configurator.clusterExtension
) {
env.logDebug(
`CRC match for file ${result.filePath} (${result.crc}), skipping parsing.`
)
delete result.result
}
}
let sessionPackages = await queryPackage.getSessionZclPackages(
db,
sessionId
)
let packageSet = new Set()
sessionPackages.map((sessionPackage) => {
packageSet.add(sessionPackage.packageRef)
})
let collectedStructItems = []
let laterPromises = await processParsedZclData(
db,
result,
packageSet,
{},
collectedStructItems
)
await Promise.all(
laterPromises.flat(1).map((promise) => {
if (promise != null && promise != undefined) return promise()
})
)
// Process collected struct items now because other data types have been loaded.
if (collectedStructItems.length > 0) {
let processStructItemsPromises = collectedStructItems.map((args) =>
processStructItems(...args)
)
await Promise.all(processStructItemsPromises)
}
// Check if session partition for package exists. If not then add it.
let sessionPartitionInfoForNewPackage =
await querySession.selectSessionPartitionInfoFromPackageId(
db,
sessionId,
pkgId,
false
)
let sessionPartitionId
if (sessionPartitionInfoForNewPackage.length == 0) {
// session partition does not exist for this package - adding new session partition
let sessionPartitionInfo =
await querySession.getAllSessionPartitionInfoForSession(db, sessionId)
sessionPartitionId = await querySession.insertSessionPartition(
db,
sessionId,
sessionPartitionInfo.length + 1
)
} else {
// session partition exists for this package
sessionPartitionId =
sessionPartitionInfoForNewPackage[0].sessionPartitionId
}
await queryPackage.insertSessionPackage(db, sessionPartitionId, pkgId, true)
await zclLoader.processZclPostLoading(db, pkgId)
// additional post-processing for custom xml
let knownPackages = sessionPackages
.filter((pkg) =>
[
dbEnum.packageType.zclProperties,
dbEnum.packageType.zclXmlStandalone
].includes(pkg.type)
)
.map((pkg) => pkg.packageRef)
await queryDeviceType.updateDeviceTypeReferencesForCustomXml(
db,
pkgId,
knownPackages,
sessionId
)
return { succeeded: true, packageId: pkgId }
} catch (err) {
env.logError(`Error reading xml file: ${filePath}\n` + err.message)
querySessionNotification.setNotification(
db,
'ERROR',
`Error reading xml file: ${filePath}, Error Message: ` + err.message,
sessionId,
1,
0
)
return { succeeded: false, err: err }
}
}
/**
* If custom device is supported, then this method creates it.
*
* @param {*} db
* @param {*} ctx
* @returns context
*/
async function processCustomZclDeviceType(db, packageId) {
let customDeviceTypes = []
customDeviceTypes.push({
domain: dbEnum.customDevice.domain,
code: dbEnum.customDevice.code,
profileId: dbEnum.customDevice.profileId,
name: dbEnum.customDevice.name,
description: dbEnum.customDevice.description
})
let existingCustomDevice =
await queryDeviceType.selectDeviceTypeByCodeAndName(
db,
packageId,
dbEnum.customDevice.code,
dbEnum.customDevice.name
)
if (existingCustomDevice == null)
await queryLoader.insertDeviceTypes(db, packageId, customDeviceTypes)
}
/**
* Load ZCL metadata
* @param {*} db
* @param {*} metafile
* @returns Promise of loaded zcl json file
*/
async function loadZclJson(db, metafile) {
return loadZclJsonOrProperties(db, metafile, true)
}
/**
* Load ZCL metadata
* @param {*} db
* @param {*} metafile
* @returns Promise of loaded zcl properties file
*/
async function loadZclProperties(db, metafile) {
return loadZclJsonOrProperties(db, metafile, false)
}
/**
* Toplevel function that loads the toplevel metafile
* and orchestrates the promise chain.
*
* @export
* @param {*} db
* @param {*} ctx The context of loading.
* @returns a Promise that resolves with the db.
*/
async function loadZclJsonOrProperties(db, metafile, isJson = false) {
let ctx = {
metadataFile: metafile,
db: db
}
let isTransactionAlreadyExisting = dbApi.isTransactionActive()
env.logDebug(`Loading Silabs zcl file: ${metafile}`)
if (!fs.existsSync(metafile)) {
throw new Error(`Can't locate: ${metafile}`)
}
if (!isTransactionAlreadyExisting) await dbApi.dbBeginTransaction(db)
try {
Object.assign(ctx, await util.readFileContentAndCrc(ctx.metadataFile))
let ret
if (isJson) {
ret = await collectDataFromJsonFile(ctx.metadataFile, ctx.data)
} else {
ret = await collectDataFromPropertiesFile(ctx.metadataFile, ctx.data)
}
Object.assign(ctx, ret)
ctx.packageId = await zclLoader.recordToplevelPackage(
db,
ctx.metadataFile,
ctx.crc,
true
)
let packageStatus = await isCrcMismatchOrPackageDoesNotExist(
db,
ctx.packageId,
ctx.zclFiles
)
if (packageStatus.isCrcMismatch || packageStatus.areSomePackagesNotLoaded) {
await queryPackage.updatePackageIsInSync(db, ctx.packageId, 0)
ctx.packageId = await zclLoader.recordToplevelPackage(
db,
ctx.metadataFile,
ctx.crc,
false
)
}
if (
ctx.version != null ||
ctx.category != null ||
ctx.description != null
) {
await zclLoader.recordVersion(
db,
ctx.packageId,
ctx.version,
ctx.category,
ctx.description
)
}
// Load the new XML files and collect which clusters were already loaded,
// so that they can be ommited while loading the old files.
let newFileResult = await newDataModel.parseNewXmlFiles(
db,
ctx.packageId,
ctx.newXmlFile
)
ctx.clustersLoadedFromNewFiles = newFileResult.clusterIdsLoaded
ctx.newFileErrors = newFileResult.errorFiles
await parseZclFiles(db, ctx.packageId, ctx.zclFiles, ctx)
// Validate that our attributeAccessInterfaceAttributes, if present, is
// sane.
if (ctx.attributeAccessInterfaceAttributes) {
let all_known_clusters = await queryZcl.selectAllClusters(
db,
ctx.packageId
)
for (let clusterName of Object.keys(
ctx.attributeAccessInterfaceAttributes
)) {
let known_cluster = all_known_clusters.find(
(c) => c.name == clusterName
)
if (!known_cluster) {
throw new Error(
`\n\nUnknown cluster "${clusterName}" in attributeAccessInterfaceAttributes\n\n`
)
}
let known_cluster_attributes =
await queryZcl.selectAttributesByClusterIdIncludingGlobal(
db,
known_cluster.id,
ctx.packageId
)
for (let attrName of ctx.attributeAccessInterfaceAttributes[
clusterName
]) {
if (!known_cluster_attributes.find((a) => a.name == attrName)) {
throw new Error(
`\n\nUnknown attribute "${attrName}" in attributeAccessInterfaceAttributes["${clusterName}"]\n\n`
)
}
}
}
}
if (ctx.manufacturersXml) {
await parseManufacturerData(db, ctx.packageId, ctx.manufacturersXml)
}
if (ctx.profilesXml) {
await parseProfilesData(db, ctx.packageId, ctx.profilesXml)
}
if (ctx.supportCustomZclDevice) {
await processCustomZclDeviceType(db, ctx.packageId)
}
if (ctx.options) {
await parseOptions(db, ctx.packageId, ctx.options)
}
if (ctx.defaults) {
await parseDefaults(db, ctx.packageId, ctx.defaults)
}
if (ctx.attributeAccessInterfaceAttributes) {
await parseattributeAccessInterfaceAttributes(
db,
ctx.packageId,
ctx.attributeAccessInterfaceAttributes
)
}
if (ctx.featureFlags) {
await parseFeatureFlags(db, ctx.packageId, ctx.featureFlags)
}
if (ctx.uiOptions) {
await parseUiOptions(db, ctx.packageId, ctx.uiOptions)
}
} catch (err) {
env.logError(err)
queryPackageNotification.setNotification(db, 'ERROR', err, ctx.packageId, 1)
throw err
} finally {
if (!isTransactionAlreadyExisting) await dbApi.dbCommit(db)
}
return ctx
}
exports.loadIndividualSilabsFile = loadIndividualSilabsFile
exports.loadZclJson = loadZclJson
exports.loadZclProperties = loadZclProperties
exports.processStructItems = processStructItems