blob: b973f3e8b2569f3810ff841f8c0a0a31efc29843 [file] [log] [blame]
/**
*
* 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 functionality that reads a .isc file
*
* @module Import API: Imports data from a file.
*/
const path = require('path')
const queryConfig = require('../db/query-config')
const queryEndpoint = require('../db/query-endpoint')
const queryZcl = require('../db/query-zcl')
const queryDeviceType = require('../db/query-device-type')
const queryAttribute = require('../db/query-attribute')
const queryCommand = require('../db/query-command')
const queryPackage = require('../db/query-package')
const querySession = require('../db/query-session')
const util = require('../util/util')
const dbEnum = require('../../src-shared/db-enum')
const restApi = require('../../src-shared/rest-api')
const env = require('../util/env')
const querySessionNotification = require('../db/query-session-notification.js')
const queryPackageNotification = require('../db/query-package-notification')
/**
* Locates or adds an attribute, and returns it.
* at contains clusterId, attributeId, isClient, mfgCode and possibly value
* @param {*} state
*/
function locateAttribute(state, at) {
let match = null
state.attributeType.forEach((a) => {
if (
at.clusterCode == a.clusterCode &&
at.attributeCode == a.attributeCode &&
at.isClient == a.isClient
) {
match = a
}
})
if (match == null) {
state.attributeType.push(at)
return at
} else {
return match
}
}
/**
* Parses attribute string in a form:
* cl:0xABCD, at:0xABCD, di: [client|server], mf:0xABCD
*
* @param {*} attributeString
* @param {*} [value=null]
*/
function parseAttribute(attributeString, value = null) {
let at = {}
attributeString
.split(',')
.map((x) => x.trim())
.forEach((el) => {
if (el.startsWith('cl:')) {
at.clusterCode = parseInt(el.substring(3))
} else if (el.startsWith('at:')) {
at.attributeCode = parseInt(el.substring(3))
} else if (el.startsWith('di:')) {
at.side =
el.substring(3).trim() == 'client'
? dbEnum.side.client
: dbEnum.side.server
} else if (el.startsWith('mf:')) {
at.mfgCode = parseInt(el.substring(3))
}
})
at.storageOption = dbEnum.storageOption.ram
if (value != null) {
at.value = value
}
return at
}
/**
* Logic that parses data out of an ISC file into a java object
*
* @param {*} state
* @param {*} line
*/
function parseZclAfv2Line(state, line) {
if (line.startsWith('configuredEndpoint:')) {
if (!('endpoint' in state)) {
state.endpoint = []
}
// configuredEndpoint:*ep:1,pi: -1,di:-1,dv:1,ept:Centralized,nwk:Primary
let tokens = line.substring('configuredEndpoint:'.length).split(',')
let endpoint = {}
tokens.forEach((tok) => {
if (tok.startsWith('ep:')) {
endpoint.endpoint = parseInt(tok.substring('ep:'.length))
} else if (tok.startsWith('*ep:')) {
endpoint.endpoint = parseInt(tok.substring('*ep:'.length))
} else if (tok.startsWith('pi:')) {
// This might be -1 and should be overriden from the actual device from the endpoint type
endpoint.profileId = parseInt(tok.substring('pi:'.length))
} else if (tok.startsWith('di:')) {
// This might be -1 and should be overriden from the actual device from the endpoint type
endpoint.deviceId = parseInt(tok.substring('di:'.length))
} else if (tok.startsWith('dv:')) {
endpoint.deviceVersion = parseInt(tok.substring('dv:'.length))
} else if (tok.startsWith('ept:')) {
endpoint.endpointType = tok.substring('ept:'.length)
} else if (tok.startsWith('nwk:')) {
let network = tok.substring('nwk:'.length)
let networkId = state.networks.indexOf(network)
if (networkId == -1) {
state.networks.push(network)
networkId = state.networks.indexOf(network)
}
endpoint.network = networkId
}
})
state.endpoint.push(endpoint)
} else if (line.startsWith('beginEndpointType:')) {
// Create a temporary state.endpointType
state.endpointType = {
typeName: line.substring('beginEndpointType:'.length),
clusterOverride: []
}
} else if (line.startsWith('endEndpointType')) {
// Stick the endpoint into `state.endpointTypes[endpointType.typeName]'
if (!('endpointTypes' in state)) {
state.endpointTypes = {}
}
state.endpointTypes[state.endpointType.typeName] = state.endpointType
delete state.endpointType
} else if (line.startsWith('device:')) {
state.endpointType.device = line.substring('device:'.length)
} else if (line.startsWith('deviceId:')) {
state.endpointType.deviceId = parseInt(line.substring('deviceId:'.length))
} else if (line.startsWith('profileId:')) {
state.endpointType.profileId = parseInt(line.substring('profileId:'.length))
} else if (line.startsWith('overrideClientCluster:')) {
let idOnOff = line.substring('overrideClientCluster:'.length).split(',')
let override = {
clusterId: parseInt(idOnOff[0]),
isIncluded: idOnOff[1] == 'yes',
side: dbEnum.side.client
}
state.endpointType.clusterOverride.push(override)
} else if (line.startsWith('overrideServerCluster:')) {
let idOnOff = line.substring('overrideServerCluster:'.length).split(',')
let override = {
clusterId: parseInt(idOnOff[0]),
isIncluded: idOnOff[1] == 'yes',
side: dbEnum.side.server
}
state.endpointType.clusterOverride.push(override)
} else if (line.startsWith('intMap:DefaultResponsePolicy = ')) {
let drp = parseInt(line.slice('intMap:DefaultResponsePolicy = '.length))
switch (drp) {
case 0:
state.sessionKey.defaultResponsePolicy = 'always'
break
case 1:
state.sessionKey.defaultResponsePolicy = 'conditional'
break
case 2:
state.sessionKey.defaultResponsePolicy = 'never'
break
}
} else if (line == 'beginAttributeDefaults') {
state.parseState = line
} else if (line == 'endAttributeDefaults') {
state.parseState = 'zclAfv2'
} else if (line == 'beginAttributeDefaultReportingConfig') {
state.parseState = line
} else if (line == 'endAttributeDefaultReportingConfig') {
state.parseState = 'zclAfv2'
} else if (line == 'beginAttrList:EXTERNALLY_SAVED') {
state.parseState = line.substring('beginAttrList:'.length)
} else if (line == 'endAttrList:EXTERNALLY_SAVED') {
state.parseState = 'zclAfv2'
} else if (line == 'beginAttrList:OPTIONAL') {
state.parseState = line.substring('beginAttrList:'.length)
} else if (line == 'endAttrList:OPTIONAL') {
state.parseState = 'zclAfv2'
} else if (line == 'beginAttrList:SINGLETON') {
state.parseState = line.substring('beginAttrList:'.length)
} else if (line == 'endAttrList:SINGLETON') {
state.parseState = 'zclAfv2'
} else if (line == 'beginAttrList:BOUNDED') {
state.parseState = line.substring('beginAttrList:'.length)
} else if (line == 'endAttrList:BOUNDED') {
state.parseState = 'zclAfv2'
} else if (line == 'beginAttrList:SAVED_TO_FLASH') {
state.parseState = line.substring('beginAttrList:'.length)
} else if (line == 'endAttrList:SAVED_TO_FLASH') {
state.parseState = 'zclAfv2'
} else if (line == 'beginAttrList:REPORTABLE') {
state.parseState = line.substring('beginAttrList:'.length)
} else if (line == 'endAttrList:REPORTABLE') {
state.parseState = 'zclAfv2'
} else if (state.parseState == 'beginAttributeDefaults') {
let arr = line.split('=>').map((x) => x.trim())
let at = parseAttribute(arr[0], arr[1])
locateAttribute(state, at).defaultValue = at.value
} else if (state.parseState == 'beginAttributeDefaultReportingConfig') {
let arr = line.split('=>').map((x) => x.trim())
// Now parse arr[1], which is min,max:change
let rpt = {}
let splits = arr[1].split(':')
let splits2 = splits[0].split(',')
rpt.reportableChange = splits[1]
rpt.minInterval = splits2[0]
rpt.maxInterval = splits2[1]
let at = parseAttribute(arr[0], rpt)
at = locateAttribute(state, at)
at.minInterval = parseInt(rpt.minInterval)
at.maxInterval = parseInt(rpt.maxInterval)
at.reportableChange = parseInt(rpt.reportableChange)
} else if (state.parseState == 'EXTERNALLY_SAVED') {
let at = parseAttribute(line.trim())
locateAttribute(state, at).storageOption = dbEnum.storageOption.external
} else if (state.parseState == 'OPTIONAL') {
let at = parseAttribute(line.trim())
locateAttribute(state, at).isOptional = true
} else if (state.parseState == 'SINGLETON') {
let at = parseAttribute(line.trim())
locateAttribute(state, at).isSingleton = true
} else if (state.parseState == 'BOUNDED') {
let at = parseAttribute(line.trim())
locateAttribute(state, at).bounded = true
} else if (state.parseState == 'SAVED_TO_FLASH') {
let at = parseAttribute(line.trim())
locateAttribute(state, at).storageOption = dbEnum.storageOption.nvm
} else if (state.parseState == 'REPORTABLE') {
let at = parseAttribute(line.trim())
locateAttribute(state, at).reportable = true
}
}
/**
* Function that deals with the zcl customizer data inside the ISC file
*
* @param {*} state
* @param {*} line
*/
function parseZclCustomizer(state, line) {
//console.log(`zclCustomizer:${line}`)
}
/**
* Toplevel parser that ignore anything except the two setups that are
* ZCL relevant.
*
* @param {*} filePath
* @param {*} data
* @returns promise of read ISC data
*/
async function readIscData(filePath, data, zclMetafile) {
const lines = data.toString().split(/\r?\n/)
const errorLines = []
let parser = null
let state = {
log: [],
filePath: filePath,
featureLevel: 0,
keyValuePairs: [],
loader: iscDataLoader,
parseState: 'init',
// These are not the same as with zap files
attributeType: [],
zclMetafile: zclMetafile,
sessionKey: {},
networks: []
}
state.log.push({
timestamp: new Date().toISOString(),
log: `Imported from ${path.basename(filePath)}`
})
lines.forEach((line) => {
if (line == '{setupId:zclAfv2') {
parser = parseZclAfv2Line
state.parseState = 'zclAfv2'
return
}
if (line == '{setupId:zclCustomizer') {
parser = parseZclCustomizer
state.parseState = 'zclCustomizer'
return
}
if (state.parseState != 'init' && line == '}') {
parser = null
state.parseState = 'nonSetup'
return
}
if (parser != null) {
try {
parser(state, line)
} catch (msg) {
errorLines.push(msg)
}
}
})
if (state.parseState == 'init') {
const S =
'Error importing the file: there is no usable ZCL content in this file.'
state.log.push(S)
throw new Error(S)
}
delete state.parseState
if (errorLines.length > 0) {
throw new Error(
'Error while importing the file:\n - ' + errorLines.join('\n - ')
)
} else {
return state
}
}
/**
* Load individual endpoint types.
*
* @param {*} db
* @param {*} sessionId
* @param {*} zclPackages Array of package IDs for zcl queries.
* @param {*} endpointType
*/
async function loadEndpointType(db, sessionId, packageId, endpointType) {
let deviceName = endpointType.device
let deviceCode = endpointType.deviceId
let deviceVersion = endpointType.deviceVersion
if (endpointType.device == 'zcustom' && endpointType.deviceId != -1) {
deviceName = await queryDeviceType
.selectDeviceTypeByCode(db, packageId, endpointType.deviceId)
.then((dev) => (dev ? dev.name : deviceName))
}
let dev
if (isCustomDevice(deviceName, deviceCode)) {
dev = await queryDeviceType.selectDeviceTypeByCodeAndName(
db,
packageId,
dbEnum.customDevice.code,
dbEnum.customDevice.name
)
} else {
dev = await queryDeviceType.selectDeviceTypeByCodeAndName(
db,
packageId,
deviceCode,
deviceName
)
}
if (dev == null)
throw new Error(`Unknown device type: ${deviceName} / ${deviceCode}`)
// Get session partition given the device type reference
let sessionPartitionInfo =
await querySession.selectSessionPartitionInfoFromDeviceType(
db,
sessionId,
dev.id
)
return queryConfig.insertEndpointType(
db,
sessionPartitionInfo[0],
endpointType.typeName,
dev.id,
dev.code,
deviceVersion,
false
)
}
/**
* check if device is a custom device type.
*
* @param {*} deviceName
* @param {*} deviceCode
* @returns boolean
*/
function isCustomDevice(deviceName, deviceCode) {
return deviceName == 'zcustom'
}
/**
* Load and endpoint type attribute entry.
*
* @param {*} db
* @param {*} endpointTypeId
* @param {*} packageId
* @param {*} at
* @returns Promise of an endpoint type attribute update
*/
async function loadSingleAttribute(db, endpointTypeId, packageId, at) {
let id = await queryConfig.selectEndpointTypeAttributeId(
db,
endpointTypeId,
packageId,
at.clusterCode,
at.attributeCode,
at.side,
at.mfgCode
)
if (id == null) {
if (at.isOptional) {
// We need to load this thing.
let cluster = await queryZcl.selectClusterByCode(
db,
packageId,
at.clusterCode,
at.mfgCode
)
let attribute = await queryAttribute.selectAttributeByCode(
db,
packageId,
at.clusterCode,
at.attributeCode,
at.mfgCode
)
if (cluster == null || attribute == null) {
let message = `Could not resolve attribute ${at.clusterCode} / ${at.attributeCode}`
env.logWarning(message)
queryPackageNotification.setNotification(
db,
'WARNING',
message,
packageId,
2
)
return
}
let clusterRef = cluster.id
let attributeRef = attribute.id
id = await queryConfig.insertOrUpdateAttributeState(
db,
endpointTypeId,
clusterRef,
at.side,
attributeRef,
[{ key: restApi.updateKey.attributeSelected, value: 1 }],
attribute.reportMinInterval,
attribute.reportMaxInterval,
attribute.reportableChange
)
} else {
// This is ok: we are iterating over all endpoint type ids,
// since ISC file doesn't really specifically override attribute
// for every given endpoint type. So if we are looking at
// the endpoint type which simply doesn't have this
// attribute, so be it. Move on.
return
}
}
let keyValuePairs = []
if ('storageOption' in at) {
keyValuePairs.push([restApi.updateKey.attributeStorage, at.storageOption])
}
if ('defaultValue' in at) {
keyValuePairs.push([restApi.updateKey.attributeDefault, at.defaultValue])
}
let reportable = false
if ('minInterval' in at) {
keyValuePairs.push([restApi.updateKey.attributeReportMin, at.minInterval])
reportable = true
}
if ('maxInterval' in at) {
keyValuePairs.push([restApi.updateKey.attributeReportMax, at.maxInterval])
reportable = true
}
if ('reportableChange' in at) {
keyValuePairs.push([
restApi.updateKey.attributeReportChange,
at.reportableChange
])
reportable = true
}
if ('isSingleton' in at) {
keyValuePairs.push([restApi.updateKey.attributeSingleton, at.isSingleton])
}
if ('bounded' in at) {
keyValuePairs.push([restApi.updateKey.attributeBounded, at.bounded])
}
if (reportable) {
keyValuePairs.push([restApi.updateKey.attributeReporting, 1])
}
return queryConfig.updateEndpointTypeAttribute(db, id, keyValuePairs)
}
/**
* Loads all implemented commands for a single endpoint.
*
* @param {*} db
* @param {*} zclPackageId
* @param {*} state
* @param {*} commandExtensions
* @param {*} endpointTypeId
*/
async function loadImplementedCommandsForEndpoint(
db,
zclPackageId,
state,
commandExtensions,
endpointTypeId
) {
let codes = {}
for (const ext of commandExtensions.defaults) {
if (ext.value == 1) {
if (!(ext.parentCode in codes)) {
codes[ext.parentCode] = []
}
codes[ext.parentCode].push(ext.entityCode)
}
}
let insertionPromises = []
// We have an array of codes now that we have to load into the database.
for (const c of Object.keys(codes)) {
let clusterCode = parseInt(c)
let commandIds = codes[c]
let cluster = await queryZcl.selectClusterByCode(
db,
zclPackageId,
clusterCode
)
for (const commandCode of Object.keys(commandIds)) {
let command = await queryCommand.selectCommandByCode(
db,
zclPackageId,
clusterCode,
commandCode
)
if (cluster != null && command != null) {
// Inject the corresponding cluster command combo into endpoint
let p = queryConfig.insertOrUpdateCommandState(
db,
endpointTypeId,
cluster.id,
command.source,
command.id,
1,
true
)
insertionPromises.push(p)
}
}
}
return Promise.all(insertionPromises)
}
/**
* This method goes over the endpoint type and the state, and enables
* commands that belong to enabled clusters and are listed in the
* commandExtensions as "implemented".
* @param {*} db
* @param {*} zclPackageId
* @param {*} state
* @param {*} commandExtensions
* @param {*} endpointTypeIdArray
*/
async function loadImplementedCommandsExtensions(
db,
zclPackageId,
state,
commandExtensions,
endpointTypeIdArray
) {
let promises = []
for (let endpointTypeId of endpointTypeIdArray) {
promises.push(
loadImplementedCommandsForEndpoint(
db,
zclPackageId,
state,
commandExtensions,
endpointTypeId
)
)
}
return Promise.all(promises)
}
/**
* This method resolves promises that contain all the
* queries that are needed to load the attribute state
*
* @param {*} db
* @param {*} state
* @param {*} sessionId
*/
async function loadCommands(
db,
state,
zclPackageId,
genPackageId,
endpointTypeIdArray
) {
if (genPackageId != null) {
let commandExtensions =
await queryPackage.selectPackageExtensionByPropertyAndEntity(
db,
genPackageId,
'implementedCommands',
dbEnum.packageExtensionEntity.command
)
if (
commandExtensions != null &&
commandExtensions.defaults != null &&
commandExtensions.defaults.length > 0
) {
await loadImplementedCommandsExtensions(
db,
zclPackageId,
state,
commandExtensions,
endpointTypeIdArray
)
}
}
}
/**
* This method resolves promises that contain all the
* queries that are needed to load the attribute state
*
* @param {*} db
* @param {*} state
* @param {*} sessionId
*/
async function loadAttributes(db, state, packageId, endpointTypeIdArray) {
let promises = []
if (state.attributeType.length > 0 && endpointTypeIdArray.length > 0) {
endpointTypeIdArray.forEach((endpointTypeId) => {
state.attributeType.forEach((at) => {
promises.push(loadSingleAttribute(db, endpointTypeId, packageId, at))
})
})
}
if (promises.length > 0) {
return Promise.all(promises)
} else {
return []
}
}
/**
* Loads the session key values from the keyValues object
* @param {*} db
* @param {*} sessionId
* @param {*} keyValues
*/
async function loadSessionKeyValues(db, sessionId, keyValues) {
return querySession.insertSessionKeyValues(db, sessionId, keyValues)
}
/**
* Function that actually loads the data out of a state object.
* Session at this point is blank, and has no packages.
*
* @param {*} db
* @param {*} state
* @param {*} sessionId
*/
async function iscDataLoader(db, state, sessionId) {
let endpointTypes = state.endpointTypes
let promises = []
await querySessionNotification.setNotification(
db,
'UPGRADE',
'ISC FILE UPGRADED TO ZAP FILE. PLEASE SAVE AS TO SAVE OFF NEWLY CREATED ZAP FILE.',
sessionId,
2
)
// We don't have the package info inside ISC file, so we
// do our best here.
await util.ensurePackagesAndPopulateSessionOptions(db, sessionId, {
zcl: state.zclMetafile,
template: null
})
let zclPackages = await queryPackage.getSessionPackagesByType(
db,
sessionId,
dbEnum.packageType.zclProperties
)
let genPackages = await queryPackage.getSessionPackagesByType(
db,
sessionId,
dbEnum.packageType.genTemplatesJson
)
if (zclPackages.length == 0) {
throw new Error('No zcl packages found for ISC import.')
}
let genPackageId = null
if (genPackages.length == 0) {
env.logWarning('No gen packages, missing the extensions matching.')
querySessionNotification.setNotification(
db,
'WARNING',
'No gen packages, missing the extensions matching.',
sessionId,
2,
0
)
} else {
genPackageId = genPackages[0].id
}
let zclPackageId = zclPackages[0].id
// Remove endpoint types that are not used.
let usedEndpointTypes = state.endpoint.map((ep) => ep.endpointType)
for (let endpointTypeKey of Object.keys(endpointTypes)) {
if (!usedEndpointTypes.includes(endpointTypeKey)) {
delete endpointTypes[endpointTypeKey]
}
}
for (let key of Object.keys(endpointTypes)) {
promises.push(
loadEndpointType(db, sessionId, zclPackageId, endpointTypes[key])
.then((newEndpointTypeId) => {
return {
endpointTypeId: newEndpointTypeId,
endpointType: endpointTypes[key]
}
})
.then((endpointTypeIds) => {
// Now load the cluster configs
let clusterOverridePromises = []
endpointTypes[key].clusterOverride.forEach((cl) => {
let clusterCode = cl.clusterId
let isIncluded = cl.isIncluded
let side = cl.side
clusterOverridePromises.push(
queryConfig.setClusterIncluded(
db,
zclPackageId,
endpointTypeIds.endpointTypeId,
clusterCode,
isIncluded,
side
)
)
})
return Promise.all(clusterOverridePromises).then(
() => endpointTypeIds
)
})
)
}
let results = await Promise.all(promises)
// results is an array of "endpointTypeId"/"endpointType" objects.
let endpointInsertionPromises = []
if (state.endpoint != null)
state.endpoint.forEach((ep) => {
// insert individual endpoint
let endpointTypeId = undefined
results.forEach((res) => {
if (res.endpointType.typeName == ep.endpointType) {
endpointTypeId = res.endpointTypeId
// Now let's deal with the endpoint id and device id
if (ep.profileId == -1) {
ep.profileId = res.endpointType.profileId
}
if (ep.deviceId == -1) {
ep.deviceId = res.endpointType.deviceId
}
}
})
if (endpointTypeId != undefined) {
endpointInsertionPromises.push(
queryEndpoint
.insertEndpoint(
db,
sessionId,
ep.endpoint,
endpointTypeId,
ep.network,
ep.profileId
)
.then(() => endpointTypeId)
)
}
})
if (state.log != null) {
querySession.writeLog(db, sessionId, state.log)
}
let endpointTypeIds = await Promise.all(endpointInsertionPromises)
await loadAttributes(db, state, zclPackageId, endpointTypeIds)
await loadCommands(db, state, zclPackageId, genPackageId, endpointTypeIds)
await loadSessionKeyValues(db, sessionId, state.sessionKey)
await querySession.setSessionClean(db, sessionId)
return {
sessionId: sessionId,
zclPackageId: zclPackageId,
templateIds: genPackages,
errors: [],
warnings: []
}
}
exports.readIscData = readIscData