blob: 64a7951183b95aaadebbddae52df2cfea1b0af71 [file] [log] [blame]
/**
*
* Copyright (c) 2023 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.
*/
/**
* Formats ZAP file in a certain format.
*
* @module Format API: Formats .zap file
*/
const _ = require('lodash')
const types = require('../util/types')
/**
* Converts attribute storage string to internal representation
*
* @param {*} a
* @returns attribute object
*/
function unpackAttribute(a) {
let data
if (a.includes('=>')) {
data = a.split('=>')[0]
} else {
data = a
}
let toks = data.split(' | ').map((x) => x.trim())
if (toks.length != 12) throw new Error(`Invalid format: ${a}`)
let attr = {}
attr.included = toks[0] === '+' ? 1 : 0
attr.code = types.hexStringToInt(toks[1])
if (toks[2].length == 0) {
attr.mfgCode = null
} else {
attr.mfgCode = types.hexStringToInt(toks[2])
}
attr.side = toks[3]
attr.storageOption = toks[4] === 'Ext' ? 'External' : toks[4]
attr.singleton = toks[5] === 'singleton' ? 1 : 0
attr.bounded = toks[6] === 'bound' ? 1 : 0
attr.defaultValue = toks[7]
attr.reportable = parseInt(toks[8])
attr.minInterval = parseInt(toks[9])
attr.maxInterval = parseInt(toks[10])
attr.reportableChange = parseInt(toks[11])
return attr
}
/**
* Converts attribute object for internal representation.
*
* @param {*} a
* @returns string
*/
function packAttribute(a) {
let data = [
a.included === 1 ? '+' : '-',
types.intToHexString(a.code, 2),
a.mfgCode != null ? types.intToHexString(a.mfgCode, 2) : ' ',
a.side,
a.storageOption === 'External' ? 'Ext' : a.storageOption,
a.singleton ? 'singleton' : ' ',
a.bounded ? 'bound' : ' ',
a.defaultValue.padStart(20, ' '),
a.reportable,
a.minInterval,
a.maxInterval,
a.reportableChange
].join(' | ')
return `${data} => ${a.name} [${a.type}]`
}
/**
* Converts command storage string to internal representation
*
* @param {*} c
* @returns command object
*/
function unpackCommand(c) {
let data
if (c.includes('=>')) {
data = c.split('=>')[0]
} else {
data = c
}
let toks = data.split(' | ').map((x) => x.trim())
if (toks.length != 5) throw new Error(`Invalid format: ${a}`)
let cmd = {}
cmd.code = types.hexStringToInt(toks[0])
if (toks[1].length == 0) {
cmd.mfgCode = null
} else {
cmd.mfgCode = types.hexStringToInt(toks[1])
}
cmd.source = toks[2]
cmd.incoming = toks[3] === '1' ? 1 : 0
cmd.outgoing = toks[4] === '1' ? 1 : 0
return cmd
}
/**
* Converts command object for file representation.
*
* @param {*} cmd
* @returns string
*/
function packCommand(cmd) {
let data = [
types.intToHexString(cmd.code, 2),
cmd.mfgCode != null ? types.intToHexString(cmd.mfgCode, 2) : ' ',
cmd.source,
cmd.incoming,
cmd.outgoing
].join(' | ')
return `${data} => ${cmd.name}`
}
/**
* Convert string representation to internal object representation
*
* @param {*} ev
* @returns event object
*/
function unpackEvent(ev) {
let data
if (ev.includes('=>')) {
data = ev.split('=>')[0]
} else {
data = ev
}
let toks = data.split(' | ').map((x) => x.trim())
if (toks.length != 4) throw new Error(`Invalid format: ${a}`)
let evnt = {}
evnt.included = toks[0] === '+' ? 1 : 0
evnt.code = types.hexStringToInt(toks[1])
if (toks[2].length == 0) {
evnt.mfgCode = null
} else {
evnt.mfgCode = types.hexStringToInt(toks[2])
}
evnt.side = toks[3]
return evnt
}
/**
* Converts event object for file representation
*
* @param {*} ev
* @returns String
*/
function packEvent(ev) {
let data = [
ev.included === 1 ? '+' : '-',
types.intToHexString(ev.code, 2),
ev.mfgCode != null ? types.intToHexString(ev.mfgCode, 2) : ' ',
ev.side
].join(' | ')
return `${data} => ${ev.name}`
}
/**
* Converts the key value pairs in the file into internal representation
*
* @param {*} keyValuePairs
* @returns array of objects
*/
function unpackKeyValuePairs(keyValuePairs) {
let kvps = []
for (let kvp of keyValuePairs) {
if (_.isString(kvp)) {
let pair = kvp.split('=').map((x) => x.trim())
kvps.push({
key: pair[0],
value: pair[1]
})
} else {
kvps.push(kvp)
}
}
return kvps
}
/**
* Packs key value pairs for extenrnal representation
*
* @param {*} keyValuePairs
* @returns none
*/
function packKeyValuePairs(keyValuePairs) {
let props = []
for (let kvp of keyValuePairs) {
props.push(`${kvp.key} = ${kvp.value}`)
}
return props
}
/**
* Cleanses toplevel cluster data.
*
* @param {*} c
*/
function cleanseCluster(c) {
c.code = '0x' + c.code.toString(16).padStart(4, '0')
if (c.mfgCode != null) {
c.mfgCode = '0x' + c.mfgCode.toString(16).padStart(4, '0')
} else {
delete c.mfgCode
}
}
/**
* Uncleanses the toplevel cluster data.
*
* @param {*} c
*/
function uncleanseCluster(c) {
if (_.isString(c.code)) {
let code = c.code
if (code.startsWith('0x')) code = code.substring(2)
c.code = parseInt(code, 16)
}
if (_.isString(c.mfgCode)) {
let code = c.mfgCode
if (code.startsWith('0x')) code = code.substring(2)
c.mfgCode = parseInt(code, 16)
}
}
/**
* This function gets the state from database and converts it for a given file format.
*
* @param {*} state
* @param {*} fileFormat
*/
function convertToFile(state) {
if (state.fileFormat && state.fileFormat === 1) {
// Convert key value pairs
if (state.keyValuePairs) {
state.keyValuePairs = packKeyValuePairs(state.keyValuePairs)
}
for (let ept of state.endpointTypes) {
// Now cleanse the clusters
for (let c of ept.clusters) {
cleanseCluster(c)
}
for (let c of ept.clusters) {
// Now we convert all the attributes...
if (c.attributes) {
let atts = []
for (let a of c.attributes) {
atts.push(packAttribute(a))
}
c.attributes = atts
}
// ... and commands...
if (c.commands) {
let cmds = []
for (let cmd of c.commands) {
cmds.push(packCommand(cmd))
}
c.commands = cmds
}
// ... and events.
if (c.events) {
let evs = []
for (let ev of c.events) {
evs.push(packEvent(ev))
}
c.events = evs
}
}
}
return state
} else if (state.fileFormat && state.fileFormat === 2) {
for (let ept of state.endpointTypes) {
let enabledClusters = ept.clusters.filter((c) => c.enabled)
ept.clusters = enabledClusters
for (let c of enabledClusters) {
let enabledAttributes = c.attributes
? c.attributes.filter((a) => a.included)
: null
if (enabledAttributes) {
c.attributes = enabledAttributes
}
// ... and commands...
let enabledCommands = c.commands
? c.commands.filter((c) => c.isEnabled)
: null
if (enabledCommands) {
c.commands = enabledCommands
}
// ... and events.
let enabledEvents = c.events ? c.events.filter((e) => e.included) : null
if (enabledEvents) {
c.events = enabledEvents
}
}
}
return state
} else {
return state
}
}
/**
* Updates the clusters with the new command format where there is isIncoming
* and isEnabled instead of incoming and outgoing
* @param {*} cluster
* @param {*} clusterToCommandMap
* @returns state or null
*/
function updateCommands(cluster, clusterToCommandMap) {
if (cluster.commands) {
for (let cmd of cluster.commands) {
// Returning the state since the .zap file has already been updated
// to the new format based on the schema correction for
// endpoint_type_command
if ('isIncoming' in cmd) {
return true
}
// Create the cmd in the new format where there is no incoming and outgoing
let tempCmdOutgoing = { ...cmd }
let tempCmdIncoming = { ...cmd }
if (tempCmdOutgoing.outgoing) {
tempCmdOutgoing.isIncoming = 0
tempCmdOutgoing.isEnabled = cmd.outgoing
if (cluster.side in clusterToCommandMap[cluster.name]) {
clusterToCommandMap[cluster.name][cluster.side].push(tempCmdOutgoing)
} else {
clusterToCommandMap[cluster.name][cluster.side] = [tempCmdOutgoing]
}
}
if (tempCmdIncoming.incoming) {
tempCmdIncoming.isIncoming = 1
tempCmdIncoming.isEnabled = cmd.incoming
let clusterSide = cmd.source == 'client' ? 'server' : 'client'
if (clusterSide in clusterToCommandMap[cluster.name]) {
clusterToCommandMap[cluster.name][clusterSide].push(tempCmdIncoming)
} else {
clusterToCommandMap[cluster.name][clusterSide] = [tempCmdIncoming]
}
}
delete tempCmdOutgoing.incoming
delete tempCmdOutgoing.outgoing
delete tempCmdIncoming.incoming
delete tempCmdIncoming.outgoing
}
}
return false
}
/**
* This function gets the JSON from the file, and converts it to the correct database state
*/
function convertFromFile(state) {
if (state.fileFormat && state.fileFormat > 0) {
// Convert key value pairs
if (state.keyValuePairs) {
state.keyValuePairs = unpackKeyValuePairs(state.keyValuePairs)
}
for (let ept of state.endpointTypes) {
let clusterToCommandMap = {}
// Now uncleanse the clusters
for (let c of ept.clusters) {
uncleanseCluster(c)
if (!(c.name in clusterToCommandMap)) {
clusterToCommandMap[c.name] = {}
}
// Now we convert all the attributes...
if (c.attributes) {
let atts = []
for (let a of c.attributes) {
if (_.isString(a)) {
atts.push(unpackAttribute(a))
} else {
atts.push(a)
}
}
c.attributes = atts
}
// ... and commands.
if (c.commands) {
let cmds = []
for (let cmd of c.commands) {
if (_.isString(cmd)) {
cmds.push(unpackCommand(cmd))
} else {
cmds.push(cmd)
}
}
c.commands = cmds
}
let updateCommandRes = updateCommands(c, clusterToCommandMap)
if (updateCommandRes) {
return state
}
// ... and events.
if (c.events) {
let evs = []
for (let ev of c.events) {
if (_.isString(ev)) {
evs.push(unpackEvent(ev))
} else {
evs.push(ev)
}
}
c.events = evs
}
}
// Use the above map to update the commands within all clusters
for (let c of ept.clusters) {
if (clusterToCommandMap[c.name][c.side]) {
c.commands = clusterToCommandMap[c.name][c.side]
} else {
c.commands = []
}
}
}
return state
} else {
// Convert key value pairs
if ('endpointTypes' in state) {
if (!Array.isArray(state.endpointTypes)) {
return state
}
for (let ept of state.endpointTypes) {
// Create a cluster to command map. Update the map correctly such that
// the right commands are stored under the right side of the cluster
// i.e. if cluster side is the same as command source then command is
// stored under the cluster side as outgoing command and if cluster side
// is not the same as command source then command is stored on the
// opposite cluster side as an incoming command.
// Note: The map is just needed for fixing the old .zap file. This is
// not needed once the .zap file is updated and saved.
let clusterToCommandMap = {}
for (let c of ept.clusters) {
if (!(c.name in clusterToCommandMap)) {
clusterToCommandMap[c.name] = {}
}
let updateCommandRes = updateCommands(c, clusterToCommandMap)
if (updateCommandRes) {
return state
}
}
// Use the above map to update the commands within all clusters
for (let c of ept.clusters) {
if (clusterToCommandMap[c.name][c.side]) {
c.commands = clusterToCommandMap[c.name][c.side]
} else {
c.commands = []
}
}
}
return state
} else {
return state
}
}
}
exports.convertFromFile = convertFromFile
exports.convertToFile = convertToFile