blob: 626f567ef2f81f419fa91b4f52e84f2779ff2bdf [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 file provides the functionality that reads the ZAP data from a JSON file
* and imports it into a database.
*
* @module Import API: Imports data from a file.
*/
const fsp = require('fs').promises
const fs = require('fs')
const path = require('path')
const importIsc = require('./import-isc.js')
const importJson = require('./import-json.js')
const dbApi = require('../db/db-api.js')
const querySession = require('../db/query-session.js')
const env = require('../util/env')
const script = require('../util/post-import.js')
const dbEnum = require('../../src-shared/db-enum')
const ff = require('./file-format.js')
const util = require('../util/util.js')
/**
* Reads the data from the file and resolves with the state object if all is good.
*
* @param {*} filePath
* @param {*} defaultZclMetafile
* @returns Promise of file reading.
*/
async function readDataFromFile(filePath, defaultZclMetafile) {
let resolvedPath = await fsp.realpath(filePath)
let data = await fsp.readFile(resolvedPath)
let stringData = data.toString().trim()
if (stringData.startsWith('{')) {
return importJson.readJsonData(filePath, data)
} else if (stringData.startsWith('#ISD')) {
return importIsc.readIscData(
filePath,
data,
defaultZclMetafile == null
? env.builtinSilabsZclMetafile()
: defaultZclMetafile
)
} else {
throw new Error(
'Invalid file format. Only .zap JSON files and ISC file format are supported.'
)
}
}
/**
* Execute the post import script oonce import is done.
*
* @param {*} db
* @param {*} sessionId
* @param {*} scriptInfo
* @returns Promise of function execution.
*/
async function executePostImportScript(db, sessionId, scriptInfo) {
let context = {
db: db,
sessionId: sessionId,
script: scriptInfo
}
return script.executeScriptFunction(script.functions.postLoad, context)
}
/**
* Merge extension file content into the base zap file content.
*
* @param {Object} baseState - The state object of the base .zap file.
* @param {Object} extensionState - The state object of the extension .zap file.
*/
function mergeZapExtension(baseState, extensionState) {
if (extensionState.endpoints) {
extensionState.endpoints.forEach((extEndpoint) => {
// Finding an endpoint using endpoint identifier linked to extension endpoint Id
const baseEndpoint = baseState.endpoints.find(
(et) => et.endpointId === extEndpoint.id
)
if (!baseEndpoint) {
return
}
// Finding the appropriate endpointType in the .zap file/base State
const baseEndpointType =
baseState.endpointTypes[baseEndpoint.endpointTypeIndex]
if (baseEndpointType) {
extEndpoint.clusters.forEach((extCluster) => {
const baseCluster = baseEndpointType.clusters.find(
(bc) => bc.code === extCluster.code
)
if (baseCluster) {
// Merge attributes
if (extCluster.attributes) {
baseCluster.attributes = baseCluster.attributes || []
extCluster.attributes.forEach((extAttr) => {
const existingAttr = baseCluster.attributes.find(
(attr) => attr.code === extAttr.code
)
if (!existingAttr) {
baseCluster.attributes.push(extAttr)
}
})
}
// Merge commands
if (extCluster.commands) {
baseCluster.commands = baseCluster.commands || []
extCluster.commands.forEach((extAttr) => {
const existingCmd = baseCluster.commands.find(
(attr) => attr.code === extAttr.code
)
if (!existingCmd) {
baseCluster.commands.push(extAttr)
}
})
}
} else {
// Add new cluster if it doesn't exist
baseEndpointType.clusters.push(extCluster)
}
})
}
})
}
}
/**
* Extract upgrade rules from ZCL packages
* @param {Array} upgradeZclPackages - Array of ZCL packages
* @returns {Array} Array of upgrade rule objects
*/
function extractUpgradeRules(upgradeZclPackages) {
let upgradeRules = []
// If more than one upgrade package is present then it is a multiprotocol
// application so upgrade rules should be added to the corresponding endpoints.
let isMultiProtocol = upgradeZclPackages.length > 1
for (const pkg of upgradeZclPackages) {
if (pkg.path) {
try {
const jsonData = JSON.parse(fs.readFileSync(pkg.path, 'utf-8'))
if (jsonData.upgradeRules !== undefined) {
const upgradeRulesJsonPath = path.resolve(
path.dirname(pkg.path),
jsonData.upgradeRules
)
try {
const upgradeRulesData = JSON.parse(
fs.readFileSync(upgradeRulesJsonPath, 'utf-8')
)
// Sorting upgrade rules by priority and then run them
upgradeRulesData.upgradeRuleScripts
.sort((a, b) => a.priority - b.priority)
.forEach((ur) => {
upgradeRules.push({
path: path.resolve(path.dirname(pkg.path), ur.path),
category: isMultiProtocol ? upgradeRulesData.category : null
})
})
} catch (error) {
env.logError(
`Error reading or parsing upgrade rules from path ${upgradeRulesJsonPath}:`,
error
)
}
}
} catch (error) {
env.logError(
`Error reading or parsing JSON from path ${pkg.path}:`,
error
)
}
}
}
return upgradeRules
}
/**
* Writes the data from the file into a new session.
* NOTE: This function does NOT initialize session packages.
*
* @export
* @param {*} db
* @param {*} filePath
* @returns a promise that resolves with the import result object that contains: sessionId, errors, warnings.
*/
async function importDataFromFile(
db,
filePath,
options = {
sessionId: null,
defaultZclMetafile: env.builtinSilabsZclMetafile(),
postImportScript: null,
packageMatch: dbEnum.packageMatch.fuzzy
}
) {
let state = await readDataFromFile(filePath, options.defaultZclMetafile)
// Merge extension files into the state
if (
options.extensionFiles &&
Array.isArray(options.extensionFiles) &&
options.extensionFiles.length > 0
) {
for (const extensionFile of options.extensionFiles) {
const extensionState = await readDataFromFile(
extensionFile,
options.defaultZclMetafile
)
mergeZapExtension(state, extensionState)
}
}
state = ff.convertFromFile(state)
// If upgrade rules are not known then figure them out.
if (!options.upgradeRuleScripts) {
// If defaultZclMetafile doesn't exist, figure it out based on filePath
if (
!options.defaultZclMetafile &&
state.package &&
Array.isArray(state.package)
) {
// Find all ZCL properties packages from the state
const zclPackages = state.package.filter(
(pkg) => pkg.type === dbEnum.packageType.zclProperties
)
if (zclPackages.length > 0) {
options.defaultZclMetafile = zclPackages
.map((pkg) => {
if (pkg.path) {
// If the path is relative, resolve it relative to the filePath directory
if (!path.isAbsolute(pkg.path)) {
return path.resolve(path.dirname(filePath), pkg.path)
} else {
return pkg.path
}
}
return null
})
.filter((path) => path !== null)
} else {
// Fallback to builtin if no ZCL package found in state
options.defaultZclMetafile = [env.builtinSilabsZclMetafile()]
}
}
// If defaultTemplateFile doesn't exist, figure it out based on filePath
if (
!options.defaultTemplateFile &&
state.package &&
Array.isArray(state.package)
) {
// Find all template packages from the state
const templatePackages = state.package.filter(
(pkg) => pkg.type === dbEnum.packageType.genTemplatesJson
)
if (templatePackages.length > 0) {
options.defaultTemplateFile = templatePackages
.map((pkg) => {
if (pkg.path) {
// If the path is relative, resolve it relative to the filePath directory
if (!path.isAbsolute(pkg.path)) {
return path.resolve(path.dirname(filePath), pkg.path)
} else {
return pkg.path
}
}
return null
})
.filter((path) => path !== null)
} else {
// Fallback to builtin if no template package found in state
options.defaultTemplateFile = [env.builtinTemplateMetafile()]
}
}
// Add upgrade package matching logic
let upgradeZclPackages = await util.getUpgradePackageMatch(
db,
options.zclProperties || options.defaultZclMetafile,
state.package,
dbEnum.packageType.zclProperties
)
let upgradeTemplatePackages = await util.getUpgradePackageMatch(
db,
options.generationTemplate || options.defaultTemplateFile,
state.package,
dbEnum.packageType.genTemplatesJson
)
let upgradeRules = extractUpgradeRules(upgradeZclPackages)
// Set upgrade rules in options if they exist
if (upgradeRules.length > 0) {
options.upgradeRuleScripts = upgradeRules
}
options.upgradeZclPackages = upgradeZclPackages
options.upgradeTemplatePackages = upgradeTemplatePackages
}
try {
await dbApi.dbBeginTransaction(db)
let sid
if (options.sessionId == null) {
sid = await querySession.createBlankSession(db)
await util.ensurePackagesAndPopulateSessionOptions(
db,
sid,
{
zcl: env.builtinSilabsZclMetafile(),
template: env.builtinTemplateMetafile(),
upgradeZclPackages: options.upgradeZclPackages,
upgradeTemplatePackages: options.upgradeTemplatePackages
},
null,
null
)
} else {
sid = options.sessionId
}
// Update endpoint type with device version and device identifier. There
// was a schema change where the device version and device identifer was
// moved into the endpoint_type_device table instead of keeping it in the
// endpoint table.
if (state.endpoints) {
state.endpoints.forEach((ep) => {
state.endpointTypes[ep.endpointTypeIndex].deviceVersion =
ep.endpointVersion
state.endpointTypes[ep.endpointTypeIndex].deviceIdentifier =
ep.deviceIdentifier
})
} else if (state.endpoint) {
// For isc file import
state.endpoint.forEach((ep) => {
if (ep.deviceId != -1) {
state.endpointTypes[ep.endpointType].deviceId = ep.deviceId
state.endpointTypes[ep.endpointType].deviceIdentifier = ep.deviceId
}
})
}
let loaderResult = await state.loader(
db,
state,
sid,
options.packageMatch,
options.defaultZclMetafile,
options.defaultTemplateFile,
options.upgradeZclPackages,
options.upgradeTemplatePackages
)
if (options.postImportScript != null) {
await executePostImportScript(db, loaderResult.sessionId, {
path: path.resolve(options.postImportScript),
category: null // When running individual scripts no need for category
})
}
if (options.upgradeRuleScripts != null) {
const upgradeScripts = options.upgradeRuleScripts
let upgradeMessages = []
for (let i = 0; i < upgradeScripts.length; i++) {
let upgradeScript = upgradeScripts[i]
if (fs.existsSync(upgradeScript.path)) {
let upgradeMessage = await executePostImportScript(
db,
loaderResult.sessionId,
upgradeScript
)
if (upgradeMessage) {
upgradeMessages.push(upgradeMessage)
}
} else {
throw new Error(
`Post import script path does not exist: ${upgradeScript.path}`
)
}
}
loaderResult.upgradeMessages = upgradeMessages
}
return loaderResult
} finally {
await dbApi.dbCommit(db)
}
}
// exports
exports.readDataFromFile = readDataFromFile
exports.importDataFromFile = importDataFromFile
exports.executePostImportScript = executePostImportScript
exports.extractUpgradeRules = extractUpgradeRules