blob: 35a757b5be1e0ff828578f4ca4d7c1151de8065a [file]
/**
*
* 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 queries for user configuration.
*
* @module DB API: user configuration queries against the database.
*/
const dbApi = require('./db-api.js')
const dbMapping = require('./db-mapping.js')
const queryPackage = require('./query-package.js')
const dbEnum = require('../../src-shared/db-enum.js')
const queryZcl = require('./query-zcl.js')
const queryUpgrade = require('../sdk/matter.js')
const queryDeviceType = require('./query-device-type')
const querySession = require('./query-session')
const queryCommand = require('./query-command.js')
const restApi = require('../../src-shared/rest-api.js')
const _ = require('lodash')
const notification = require('../db/query-session-notification.js')
/**
* Promises to update the cluster include/exclude state.
* If the entry [as defined uniquely by endpointTypeId, clusterId, side] is not there, then insert
* Else update the entry in place.
*
* @export
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusterRef
* @param {*} side
* @param {*} enabled
* @returns Promise to update the cluster exclude/include state.
*/
async function insertOrReplaceClusterState(
db,
endpointTypeId,
clusterRef,
side,
enabled
) {
return dbApi.dbInsert(
db,
`
INSERT
INTO ENDPOINT_TYPE_CLUSTER
( ENDPOINT_TYPE_REF, CLUSTER_REF, SIDE, ENABLED )
VALUES
( ?, ?, ?, ?)
ON CONFLICT
(ENDPOINT_TYPE_REF, CLUSTER_REF, SIDE)
DO UPDATE SET ENABLED = ?`,
[endpointTypeId, clusterRef, side, enabled, enabled]
)
}
/**
* Promise to get a endpoint type's clusters.
* @param {*} db
* @param {*} endpointTypeId
*/
async function selectEndpointClusters(db, endpointTypeId) {
let rows = await dbApi.dbAll(
db,
`
SELECT * FROM ENDPOINT_TYPE_CLUSTER
WHERE
ENDPOINT_TYPE_REF = ?
`,
[endpointTypeId]
)
return rows.map(dbMapping.map.endpointTypeCluster)
}
/**
* Promise to get a cluster's state.
* This must return undefined/null for if the cluster state has not been used before for the endpointType
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusterRef
* @param {*} side
*/
async function selectClusterState(db, endpointTypeId, clusterRef, side) {
return dbApi
.dbGet(
db,
`
SELECT
ENDPOINT_TYPE_CLUSTER_ID,
ENDPOINT_TYPE_REF,
CLUSTER_REF,
SIDE,
ENABLED
FROM ENDPOINT_TYPE_CLUSTER
WHERE
ENDPOINT_TYPE_REF = ? AND
CLUSTER_REF = ? AND
SIDE = ?
`,
[endpointTypeId, clusterRef, side]
)
.then(dbMapping.map.endpointTypeCluster)
}
/**
* Promise to get clusters' states for all endpoints
* This must return undefined/null for if the cluster state has not been used before for the endpointType
* @param {*} db
* @param {*} clusterRef
* @param {*} side
*/
async function selectClusterStatesForAllEndpoints(db, clusterRef, side) {
return dbApi
.dbAll(
db,
`
SELECT
ENDPOINT_TYPE_CLUSTER_ID,
ENDPOINT_TYPE_REF,
CLUSTER_REF,
SIDE,
ENABLED
FROM ENDPOINT_TYPE_CLUSTER
WHERE
CLUSTER_REF = ? AND
SIDE = ?
`,
[clusterRef, side]
)
.then((rows) => rows.map(dbMapping.map.endpointTypeCluster))
}
/**
* Promise that resolves after inserting the defaults associated with the clusterside to the database.
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusterRef
* @param {*} side
*/
async function insertClusterDefaults(db, endpointTypeId, packageId, cluster) {
let promises = []
promises.push(
resolveDefaultAttributes(db, endpointTypeId, packageId, [cluster])
)
promises.push(
resolveNonOptionalCommands(db, endpointTypeId, [cluster], packageId)
)
return Promise.all(promises)
}
/**
* Promise to update the attribute state.
* If an attribute entry [as defined uniquely by endpointTypeId and id], is not there, then create a default entry
* Afterwards, update entry.
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusterRef // We want this to be explicitly passed in, rather than derived from that static attribute due to this possibly being a global attribute.
* @param {*} side // We want this to be explicitly passed in, rather than derived from that static attribute due to this possibly being a global attribute. Note we handle bidirectional attributes with two instances
* @param {*} attributeId
* @param {*} paramValuePairArray An array of objects whose keys are [key, value]. Key is name of the column to be editted. Value is what the column should be set to. This does not handle empty arrays.
*/
async function insertOrUpdateAttributeState(
db,
endpointTypeId,
clusterRef,
side,
attributeId,
paramValuePairArray,
reportMinInterval,
reportMaxInterval,
reportableChange
) {
if (reportMinInterval === undefined || reportMinInterval === null) {
reportMinInterval = 1
}
if (reportMaxInterval === undefined || reportMaxInterval === null) {
reportMaxInterval = 0xffff - 1
}
if (reportableChange === undefined || reportableChange === null) {
reportableChange = 0
}
let cluster = await insertOrSelectDefaultEndpointTypeCluster(
db,
endpointTypeId,
clusterRef,
side
)
if (cluster == null) {
throw new Error(`Could not locate cluster: ${clusterRef}`)
}
let staticAttribute =
await queryZcl.selectAttributeByAttributeIdAndClusterRef(
db,
attributeId,
clusterRef
)
// Looking for the feature map attribute in matter and setting it as per
// the device types if default value is 0
if (
staticAttribute.code == 0xfffc &&
staticAttribute.name == 'FeatureMap' &&
staticAttribute.defaultValue == 0
) {
let featureMapDefaultValue = staticAttribute.defaultValue
let featuresOnEndpointTypeAndCluster =
await queryDeviceType.selectDeviceTypeFeaturesByEndpointTypeIdAndClusterId(
db,
endpointTypeId,
clusterRef
)
// only set featureMap bit to 1 for mandatory features
let featureMapBitsToBeEnabled = featuresOnEndpointTypeAndCluster
.filter((f) => f.conformance == 'M')
.map((f) => f.featureBit)
featureMapBitsToBeEnabled.forEach(
(featureBit) =>
(featureMapDefaultValue = featureMapDefaultValue | (1 << featureBit))
)
staticAttribute.defaultValue = featureMapDefaultValue
}
let forcedExternal = await queryUpgrade.getForcedExternalStorage(
db,
staticAttribute.packageRef
)
staticAttribute.storagePolicy =
await queryUpgrade.computeStoragePolicyNewConfig(
db,
clusterRef,
staticAttribute.storagePolicy,
forcedExternal,
staticAttribute.name
)
let storageOption = await queryUpgrade.computeStorageOptionNewConfig(
staticAttribute.storagePolicy
)
if (
staticAttribute.storagePolicy ==
dbEnum.storagePolicy.attributeAccessInterface
) {
staticAttribute.defaultValue = null
}
if (staticAttribute == null) {
throw new Error(`COULD NOT LOCATE ATTRIBUTE: ${attributeId} `)
}
let etaId = await dbApi.dbInsert(
db,
`
INSERT OR IGNORE
INTO ENDPOINT_TYPE_ATTRIBUTE (
ENDPOINT_TYPE_CLUSTER_REF,
ATTRIBUTE_REF,
DEFAULT_VALUE,
STORAGE_OPTION,
SINGLETON,
MIN_INTERVAL,
MAX_INTERVAL,
REPORTABLE_CHANGE
) VALUES (
?,
?,
?,
?,
( SELECT IS_SINGLETON FROM CLUSTER WHERE CLUSTER_ID = ? ),
?,
?,
?
)`,
[
cluster.endpointTypeClusterId,
attributeId,
staticAttribute.defaultValue ? staticAttribute.defaultValue : '',
storageOption,
clusterRef,
reportMinInterval,
reportMaxInterval,
reportableChange
]
)
if (paramValuePairArray == null || paramValuePairArray.length == 0) {
return etaId
} else {
let query =
'UPDATE ENDPOINT_TYPE_ATTRIBUTE SET ' +
getAllParamValuePairArrayClauses(paramValuePairArray) +
'WHERE ENDPOINT_TYPE_CLUSTER_REF = ? AND ATTRIBUTE_REF = ?'
await dbApi.dbUpdate(db, query, [
cluster.endpointTypeClusterId,
attributeId
])
let row = await dbApi.dbGet(
db,
`
SELECT
ETA.ENDPOINT_TYPE_ATTRIBUTE_ID
FROM
ENDPOINT_TYPE_ATTRIBUTE AS ETA
WHERE
ENDPOINT_TYPE_CLUSTER_REF = ? AND ATTRIBUTE_REF = ?`,
[cluster.endpointTypeClusterId, attributeId]
)
return row.ENDPOINT_TYPE_ATTRIBUTE_ID
}
}
/**
* Updates the endpoint type attribute table.
*
* @param {*} db
* @param {*} id
* @param {*} keyValuePairs
* @returns Promise of an endpoint type atribute table update.
*/
async function updateEndpointTypeAttribute(db, id, keyValuePairs) {
if (keyValuePairs == null || keyValuePairs.length == 0) return
let columns = keyValuePairs
.map((kv) => `${convertRestKeyToDbColumn(kv[0])} = ?`)
.join(', ')
let args = []
keyValuePairs.forEach((kv) => {
args.push(kv[1])
})
args.push(id)
let query = `UPDATE ENDPOINT_TYPE_ATTRIBUTE SET
${columns}
WHERE ENDPOINT_TYPE_ATTRIBUTE_ID = ?`
return dbApi.dbUpdate(db, query, args)
}
/**
* Based on the given key returns a database table column name.
* @param {*} key
* @returns Database table Column as String
*/
function convertRestKeyToDbColumn(key) {
switch (key) {
case restApi.updateKey.endpointId:
return 'ENDPOINT_IDENTIFIER'
case restApi.updateKey.endpointType:
return 'ENDPOINT_TYPE_REF'
case restApi.updateKey.networkId:
return 'NETWORK_IDENTIFIER'
case restApi.updateKey.profileId:
return 'PROFILE'
case restApi.updateKey.deviceId:
return 'DEVICE_IDENTIFIER'
case restApi.updateKey.deviceIdentifier:
return 'DEVICE_IDENTIFIER'
case restApi.updateKey.endpointVersion:
return 'DEVICE_VERSION'
case restApi.updateKey.deviceVersion:
return 'DEVICE_VERSION'
case restApi.updateKey.deviceTypeRef:
return 'DEVICE_TYPE_REF'
case restApi.updateKey.name:
return 'NAME'
case restApi.updateKey.attributeSelected:
return 'INCLUDED'
case restApi.updateKey.attributeSingleton:
return 'SINGLETON'
case restApi.updateKey.attributeBounded:
return 'BOUNDED'
case restApi.updateKey.attributeDefault:
return 'DEFAULT_VALUE'
case restApi.updateKey.attributeReporting:
return 'INCLUDED_REPORTABLE'
case restApi.updateKey.attributeReportMin:
return 'MIN_INTERVAL'
case restApi.updateKey.attributeReportMax:
return 'MAX_INTERVAL'
case restApi.updateKey.attributeReportChange:
return 'REPORTABLE_CHANGE'
case restApi.updateKey.attributeStorage:
return 'STORAGE_OPTION'
}
throw new Error(`Invalid rest update key: ${key}`)
}
/**
* Create a Database column name and value string which can be used in database
* queries.
*
* @param {*} paramValuePairArray
* @returns string
*/
function getAllParamValuePairArrayClauses(paramValuePairArray) {
return paramValuePairArray.reduce((currentString, paramValuePair, index) => {
if (index > 0) currentString += ','
currentString += convertRestKeyToDbColumn(paramValuePair.key)
currentString += ' = '
if (_.isBoolean(paramValuePair.value)) {
currentString += paramValuePair.value ? '1' : '0'
} else if (paramValuePair.value == '') {
currentString += false
} else if (paramValuePair.value == null) {
currentString += 'null'
} else if (Array.isArray(paramValuePair.value)) {
currentString += paramValuePair.value[0]
} else {
if (paramValuePair.type == 'text') {
currentString += "'" + paramValuePair.value + "'"
} else {
currentString += paramValuePair.value
}
}
currentString += ' '
return currentString
}, '')
}
/**
* Promise to update the command state.
* If the command entry [as defined uniquely by endpointTypeId and id], is not there, then create a default entry
* Afterwards, update entry.
*
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusterRef // Note that this is the clusterRef from CLUSTER and not the ENDPOINT_TYPE_CLUSTER
* @param {*} side // client or server
* @param {*} id
* @param {*} value
* @param {*} booleanParam
*/
async function insertOrUpdateCommandState(
db,
endpointTypeId,
clusterRef,
side,
commandId,
value,
isIncoming
) {
/*
Retrieve the cluster side and command source and based on that add incoming/outgoing, incoming is 1 and outgoing is 0
Once that is done put the value into enabled clause. See the schema changes.
For backwards compatibility. The existing inserts into the db should continue to work the same way.
*/
let cluster = await insertOrSelectDefaultEndpointTypeCluster(
db,
endpointTypeId,
clusterRef,
!isIncoming
? side
: dbEnum.source.client == side
? dbEnum.source.server
: dbEnum.source.client
)
await dbApi.dbInsert(
db,
`
INSERT OR IGNORE
INTO ENDPOINT_TYPE_COMMAND (
ENDPOINT_TYPE_CLUSTER_REF,
COMMAND_REF,
IS_INCOMING
) VALUES( ?, ?, ? )
`,
[cluster.endpointTypeClusterId, commandId, dbApi.toDbBool(isIncoming)]
)
return dbApi.dbUpdate(
db,
`
UPDATE ENDPOINT_TYPE_COMMAND
SET IS_ENABLED = ?
WHERE ENDPOINT_TYPE_CLUSTER_REF = ?
AND COMMAND_REF = ?
AND IS_INCOMING = ? `,
[
value,
cluster.endpointTypeClusterId,
commandId,
dbApi.toDbBool(isIncoming)
]
)
}
/**
* Promise to update the event state.
* If an event entry [as defined uniquely by endpointTypeId and id], is not there, then create a default entry
* Afterwards, update entry.
*
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusterRef // Note that this is the clusterRef from CLUSTER and not the ENDPOINT_TYPE_CLUSTER
* @param {*} side // client or server
* @param {*} eventId
* @param {*} value
*/
async function insertOrUpdateEventState(
db,
endpointTypeId,
clusterRef,
side,
eventId,
value
) {
let cluster = await insertOrSelectDefaultEndpointTypeCluster(
db,
endpointTypeId,
clusterRef,
side
)
await dbApi.dbInsert(
db,
`
INSERT OR IGNORE
INTO ENDPOINT_TYPE_EVENT (
ENDPOINT_TYPE_CLUSTER_REF,
EVENT_REF
) VALUES( ?, ? )
`,
[cluster.endpointTypeClusterId, eventId]
)
return dbApi.dbUpdate(
db,
`
UPDATE ENDPOINT_TYPE_EVENT
SET INCLUDED = ?
WHERE ENDPOINT_TYPE_CLUSTER_REF = ?
AND EVENT_REF = ? `,
[value, cluster.endpointTypeClusterId, eventId]
)
}
/**
* Returns a promise to update the endpoint
*
* @param {*} db
* @param {*} sessionId
* @param {*} endpointId
* @param {*} changesArray
* @returns Promise to update the endpoint
*/
async function updateEndpoint(db, sessionId, endpointId, changesArray) {
return dbApi.dbUpdate(
db,
`UPDATE ENDPOINT SET ` +
getAllParamValuePairArrayClauses(changesArray) +
`WHERE ENDPOINT_ID = ? AND SESSION_REF = ?`,
[endpointId, sessionId]
)
}
/**
* Returns a promise to update the parent endpoint of the endpoint
*
* @param {*} db
* @param {*} sessionId
* @param {*} endpointId
* @param {*} parentRef
* @returns Promise to update the endpoint's parent ref
*/
async function updateParentEndpoint(db, sessionId, endpointId, parentRef) {
return dbApi.dbUpdate(
db,
`UPDATE ENDPOINT SET PARENT_ENDPOINT_REF = ? WHERE ENDPOINT_ID = ? AND SESSION_REF = ?`,
[parentRef, endpointId, sessionId]
)
}
/**
* Returns the number of endpoints with a given endpoint_identifier and sessionid.
* Used for validation
*
* @param {*} endpointIdentifier
* @param {*} sessionId
* @returns Promise that resolves into a count.
*/
async function selectCountOfEndpointsWithGivenEndpointIdentifier(
db,
endpointIdentifier,
sessionId
) {
return dbApi
.dbGet(
db,
'SELECT COUNT(ENDPOINT_IDENTIFIER) FROM ENDPOINT WHERE ENDPOINT_IDENTIFIER = ? AND SESSION_REF = ?',
[endpointIdentifier, sessionId]
)
.then((x) => x['COUNT(ENDPOINT_IDENTIFIER)'])
}
/**
* Promises to add an endpoint type.
*
* @param {*} db
* @param {*} sessionPartitionInfo
* @param {*} name
* @param {*} deviceTypeRef
* @param {*} deviceTypeIdentifier
* @param {*} deviceTypeVersion
* @param {*} doTransaction
* @returns Promise to update endpoints.
*/
async function insertEndpointType(
db,
sessionPartitionInfo,
name,
deviceTypeRef,
deviceTypeIdentifier,
deviceTypeVersion,
doTransaction = true
) {
let deviceTypeRefs = Array.isArray(deviceTypeRef)
? deviceTypeRef
: [deviceTypeRef]
let deviceTypeIdentifiers = Array.isArray(deviceTypeIdentifier)
? deviceTypeIdentifier
: [deviceTypeIdentifier]
let deviceTypeVersions = Array.isArray(deviceTypeVersion)
? deviceTypeVersion
: [deviceTypeVersion]
// Insert endpoint type
let newEndpointTypeId = await dbApi.dbInsert(
db,
'INSERT OR REPLACE INTO ENDPOINT_TYPE ( SESSION_PARTITION_REF, NAME ) VALUES ( ?, ?)',
[sessionPartitionInfo.sessionPartitionId, name]
)
// Creating endpoint type and device type ref combinations along with order of insertion
let newEndpointTypeIdDeviceCombination = []
for (let i = 0; i < deviceTypeRefs.length; i++) {
let endpointTypeDevice = [
newEndpointTypeId,
deviceTypeRefs[i],
deviceTypeIdentifiers[i],
deviceTypeVersions[i],
i
]
newEndpointTypeIdDeviceCombination.push(endpointTypeDevice)
}
// Insert into endpoint_type_device
try {
let etd = await dbApi.dbMultiInsert(
db,
'INSERT INTO ENDPOINT_TYPE_DEVICE (ENDPOINT_TYPE_REF, DEVICE_TYPE_REF, DEVICE_IDENTIFIER, DEVICE_VERSION, DEVICE_TYPE_ORDER) VALUES (?, ?, ?, ?, ?)',
newEndpointTypeIdDeviceCombination
)
} catch (err) {
// Catching an error from a sql trigger
let isErrorStringPresent = err.includes('Error:')
notification.setNotification(
db,
'ERROR',
isErrorStringPresent ? err.split('Error:')[1] : err,
sessionPartitionInfo.sessionRef,
1,
1
)
await dbApi.dbMultiInsert(
db,
'DELETE FROM ENDPOINT_TYPE_DEVICE WHERE ENDPOINT_TYPE_REF = ? AND DEVICE_TYPE_REF = ? AND DEVICE_IDENTIFIER = ? AND DEVICE_VERSION =? AND DEVICE_TYPE_ORDER = ?',
newEndpointTypeIdDeviceCombination
)
await dbApi.dbRemove(
db,
'DELETE FROM ENDPOINT_TYPE WHERE ENDPOINT_TYPE_ID = ?',
newEndpointTypeId
)
throw new Error(err)
}
// Resolve endpointDefaults based on device type order.
for (const dtRef of deviceTypeRefs) {
await setEndpointDefaults(
db,
sessionPartitionInfo.sessionRef,
newEndpointTypeId,
dtRef,
doTransaction
)
}
return newEndpointTypeId
}
/**
* Promises to duplicate an endpoint type.
*
* @export
* @param {*} db
* @param {*} endpointTypeId
* @returns Promise to duplicate endpoint type.
*/
async function duplicateEndpointType(db, endpointTypeId) {
// Extract all the device types based on endpoint type id
let endpointTypeDeviceInfo = await dbApi.dbAll(
db,
`
SELECT
SESSION_PARTITION.SESSION_REF,
SESSION_PARTITION.SESSION_PARTITION_ID,
ENDPOINT_TYPE.NAME,
ENDPOINT_TYPE_DEVICE.DEVICE_TYPE_REF,
ENDPOINT_TYPE_DEVICE.DEVICE_IDENTIFIER,
ENDPOINT_TYPE_DEVICE.DEVICE_VERSION
FROM
ENDPOINT_TYPE
INNER JOIN
ENDPOINT_TYPE_DEVICE
ON
ENDPOINT_TYPE.ENDPOINT_TYPE_ID = ENDPOINT_TYPE_DEVICE.ENDPOINT_TYPE_REF
INNER JOIN
SESSION_PARTITION
ON
ENDPOINT_TYPE.SESSION_PARTITION_REF = SESSION_PARTITION.SESSION_PARTITION_ID
WHERE
ENDPOINT_TYPE_DEVICE.ENDPOINT_TYPE_REF = ?`,
[endpointTypeId]
)
let newEndpointTypeId = 0
if (endpointTypeDeviceInfo && endpointTypeDeviceInfo.length > 0) {
// Enter into the endpoint_type table
newEndpointTypeId = await dbApi.dbInsert(
db,
`INSERT INTO ENDPOINT_TYPE (SESSION_PARTITION_REF, NAME)
VALUES (?, ?)`,
[
endpointTypeDeviceInfo[0].SESSION_PARTITION_ID,
endpointTypeDeviceInfo[0].NAME
]
)
// Enter into the endpoint_type_device table to establish the endpoint_type
// and device type relationship
let endpointTypeDeviceInfoValues = []
endpointTypeDeviceInfo.forEach((dt, index) =>
endpointTypeDeviceInfoValues.push([
newEndpointTypeId,
dt.DEVICE_TYPE_REF,
dt.DEVICE_IDENTIFIER,
dt.DEVICE_VERSION,
index
])
)
await dbApi.dbMultiInsert(
db,
`
INSERT INTO
ENDPOINT_TYPE_DEVICE (ENDPOINT_TYPE_REF, DEVICE_TYPE_REF, DEVICE_IDENTIFIER, DEVICE_VERSION, DEVICE_TYPE_ORDER)
VALUES
(?, ?, ?, ?, ?)`,
endpointTypeDeviceInfoValues
)
}
return newEndpointTypeId
}
/**
* Promise to update an endpoint type.
* @param {*} db
* @param {*} sessionId
* @param {*} endpointTypeId
* @param {*} changesArray
*/
async function updateEndpointType(db, sessionId, endpointTypeId, changesArray) {
// Extract all changes from the changesArray in the form of
// keys(endpoint_type columns) and its values
let updatedKeys = []
let updatedValues = []
changesArray.forEach((c) => updatedKeys.push(c.key))
changesArray.forEach((c) => updatedValues.push(c.value))
// Retrieve existing values to the endpoint_type
let existingDeviceTypeInfo = await dbApi.dbAll(
db,
`
SELECT
ENDPOINT_TYPE_DEVICE.DEVICE_TYPE_REF,
ENDPOINT_TYPE_DEVICE.DEVICE_VERSION,
ENDPOINT_TYPE_DEVICE.DEVICE_IDENTIFIER
FROM
ENDPOINT_TYPE
INNER JOIN
ENDPOINT_TYPE_DEVICE
ON ENDPOINT_TYPE.ENDPOINT_TYPE_ID = ENDPOINT_TYPE_DEVICE.ENDPOINT_TYPE_REF
INNER JOIN
SESSION_PARTITION
ON
ENDPOINT_TYPE.SESSION_PARTITION_REF = SESSION_PARTITION.SESSION_PARTITION_ID
WHERE
ENDPOINT_TYPE.ENDPOINT_TYPE_ID = ? AND SESSION_PARTITION.SESSION_REF = ?
ORDER BY
ENDPOINT_TYPE_DEVICE.DEVICE_TYPE_ORDER`,
[endpointTypeId, sessionId]
)
let existingDeviceTypeRefs = existingDeviceTypeInfo.map(
(dt) => dt.DEVICE_TYPE_REF
)
let existingDeviceVersions = existingDeviceTypeInfo.map(
(dt) => dt.DEVICE_VERSION
)
let existingEndpointTypeDeviceInfoValues = []
for (let j = 0; j < existingDeviceTypeInfo.length; j++) {
existingEndpointTypeDeviceInfoValues.push([
endpointTypeId,
existingDeviceTypeInfo[j]['DEVICE_TYPE_REF'],
existingDeviceTypeInfo[j]['DEVICE_VERSION'],
existingDeviceTypeInfo[j]['DEVICE_IDENTIFIER'],
j
])
}
let updatedDeviceTypeRefs = updatedValues[0]
let updatedDeviceVersions = updatedValues[1]
let isDeviceTypeRefsUpdated =
existingDeviceTypeRefs.length != updatedDeviceTypeRefs.length ||
!existingDeviceTypeRefs.every(
(dtRef, index) => dtRef == updatedDeviceTypeRefs[index]
)
let isDeviceVersionsUpdated =
existingDeviceVersions.length != updatedDeviceVersions.length ||
!existingDeviceVersions.every(
(version, index) => version == updatedDeviceVersions[index]
)
// Make changes if device type refs or versions have been updated
if (isDeviceTypeRefsUpdated || isDeviceVersionsUpdated) {
// Delete the endpoint_type_device references based on endpoint_type_id
await dbApi.dbRemove(
db,
'DELETE FROM ENDPOINT_TYPE_DEVICE WHERE ENDPOINT_TYPE_REF = ?',
endpointTypeId
)
// Re-insert endpoint_type_device references with the new references to device types
let endpointTypeDeviceInfoValues = []
for (let i = 0; i < updatedValues[0].length; i++) {
endpointTypeDeviceInfoValues.push([
endpointTypeId,
updatedValues[0][i],
updatedValues[1][i],
updatedValues[2][i],
i
])
}
try {
await dbApi.dbMultiInsert(
db,
`
INSERT INTO
ENDPOINT_TYPE_DEVICE (ENDPOINT_TYPE_REF, DEVICE_TYPE_REF, DEVICE_VERSION, DEVICE_IDENTIFIER, DEVICE_TYPE_ORDER)
VALUES
(?, ?, ?, ?, ?)`,
endpointTypeDeviceInfoValues
)
} catch (err) {
// Catching an error from a sql trigger
let isErrorStringPresent = err.includes('Error:')
notification.setNotification(
db,
'ERROR',
isErrorStringPresent ? err.split('Error:')[1] : err,
sessionId,
1,
1
)
// Delete endpoint type devices with the latest updates
let test = await dbApi.dbMultiInsert(
db,
'DELETE FROM ENDPOINT_TYPE_DEVICE WHERE ENDPOINT_TYPE_REF = ? AND DEVICE_TYPE_REF = ? AND DEVICE_VERSION =? AND DEVICE_IDENTIFIER = ? AND DEVICE_TYPE_ORDER = ?',
endpointTypeDeviceInfoValues
)
// Re add the old device types on the endpoint before the update
await dbApi.dbMultiInsert(
db,
`
INSERT INTO
ENDPOINT_TYPE_DEVICE (ENDPOINT_TYPE_REF, DEVICE_TYPE_REF, DEVICE_VERSION, DEVICE_IDENTIFIER, DEVICE_TYPE_ORDER)
VALUES
(?, ?, ?, ?, ?)`,
existingEndpointTypeDeviceInfoValues
)
throw new Error(err)
}
// When updating the zcl device types, overwrite on top of existing configuration
// Note: Here the existing selections are not removed. For eg: the clusters which
// came from a removed zcl device type continue to exist.
if (isDeviceTypeRefsUpdated) {
for (const dtRef of updatedDeviceTypeRefs) {
await setEndpointDefaults(db, sessionId, endpointTypeId, dtRef)
}
}
}
return endpointTypeId
}
/**
* Promise to set the default attributes and clusters for a endpoint type.
* @param {*} db
* @param {*} sessionId
* @param {*} endpointTypeId
* @param {*} deviceTypeRef
* @param {*} doTransaction
*/
async function setEndpointDefaults(
db,
sessionId,
endpointTypeId,
deviceTypeRef,
doTransaction = true
) {
if (doTransaction) {
await dbApi.dbBeginTransaction(db)
}
let pkgs = await queryPackage.getSessionPackagesByType(
db,
sessionId,
dbEnum.packageType.zclProperties
)
if (pkgs == null || pkgs.length < 1)
throw new Error('Could not locate package id for a given session.')
let deviceTypeInfo =
await querySession.selectDeviceTypePackageInfoFromDeviceTypeId(
db,
deviceTypeRef
)
let endpointTypeCategory =
deviceTypeInfo.length > 0 ? deviceTypeInfo[0].category : null
let packageId = pkgs[0].id
for (let i = 0; i < pkgs.length; i++) {
if (pkgs[i].category == endpointTypeCategory) {
packageId = pkgs[i].id
break
}
}
let clusters = await queryDeviceType.selectDeviceTypeClustersByDeviceTypeRef(
db,
deviceTypeRef
)
let defaultClusters = await resolveDefaultClusters(
db,
endpointTypeId,
clusters
)
let promises = []
promises.push(
resolveDefaultDeviceTypeAttributes(db, endpointTypeId, deviceTypeRef),
resolveDefaultDeviceTypeCommands(db, endpointTypeId, deviceTypeRef),
resolveDefaultAttributes(db, endpointTypeId, packageId, defaultClusters),
resolveNonOptionalCommands(db, endpointTypeId, defaultClusters, packageId)
)
return Promise.all(promises).finally(() => {
if (doTransaction) return dbApi.dbCommit(db)
else return Promise.resolve({ defaultClusters })
})
}
/**
* Returns a promise of resolving default clusters.
*
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusters
* @returns Promise of resolved default clusters.
*/
async function resolveDefaultClusters(db, endpointTypeId, clusters) {
let promises = clusters.map((cluster) => {
let clientServerPromise = []
if (cluster.includeClient) {
clientServerPromise.push(
insertOrReplaceClusterState(
db,
endpointTypeId,
cluster.clusterRef,
dbEnum.side.client,
true
).then(() => {
return {
clusterRef: cluster.clusterRef,
side: dbEnum.side.client
}
})
)
}
if (cluster.includeServer) {
clientServerPromise.push(
insertOrReplaceClusterState(
db,
endpointTypeId,
cluster.clusterRef,
dbEnum.side.server,
true
).then(() => {
return {
clusterRef: cluster.clusterRef,
side: dbEnum.side.server
}
})
)
}
return Promise.all(clientServerPromise)
})
let allClustersResult = await Promise.all(promises)
return allClustersResult.flat()
}
/**
* Returns promise of default device type attributes.
*
* @param {*} db
* @param {*} endpointTypeId
* @param {*} deviceTypeRef
* @returns promise of default device type attributes.
*/
async function resolveDefaultDeviceTypeAttributes(
db,
endpointTypeId,
deviceTypeRef
) {
let deviceTypeAttributes =
await queryDeviceType.selectDeviceTypeAttributesByDeviceTypeRef(
db,
deviceTypeRef
)
let promises = deviceTypeAttributes.map(async (deviceAttribute) => {
if (deviceAttribute.attributeRef != null) {
let attribute = await queryZcl.selectAttributeById(
db,
deviceAttribute.attributeRef
)
let clusterRef = attribute?.clusterRef
return insertOrUpdateAttributeState(
db,
endpointTypeId,
clusterRef,
attribute.side,
deviceAttribute.attributeRef,
[
{
key: restApi.updateKey.attributeSelected,
value: true
},
{
key: restApi.updateKey.attributeReporting,
value:
deviceAttribute.reportingPolicy ==
dbEnum.reportingPolicy.mandatory ||
deviceAttribute.reportingPolicy ==
dbEnum.reportingPolicy.suggested
}
],
attribute.reportMinInterval,
attribute.reportMaxInterval,
attribute.reportableChange
)
}
})
return Promise.all(promises)
}
/**
* Initialize the command values within an endpoint type Id based on device type.
*
* @param {*} db
* @param {*} endpointTypeId
* @param {*} deviceCommand
* @returns An array of promises which update command state.
*/
async function resolveCommandState(db, endpointTypeId, deviceCommand) {
let deviceTypeCluster =
await queryDeviceType.selectDeviceTypeClusterByDeviceTypeClusterId(
db,
deviceCommand.deviceTypeClusterRef
)
if (deviceCommand.commandRef == null) return null
let command = await queryCommand.selectCommandById(
db,
deviceCommand.commandRef
)
if (command == null) return null
let promises = []
if (deviceTypeCluster.includeClient) {
promises.push(
insertOrUpdateCommandState(
db,
endpointTypeId,
command.clusterRef,
command.source,
deviceCommand.commandRef,
true,
command.source != dbEnum.source.client
)
)
}
if (deviceTypeCluster.includeServer) {
promises.push(
insertOrUpdateCommandState(
db,
endpointTypeId,
command.clusterRef,
command.source,
deviceCommand.commandRef,
true,
command.source != dbEnum.source.server
)
)
}
return Promise.all(promises)
}
/**
* Returns promise of default device type commands.
*
* @param {*} db
* @param {*} endpointTypeId
* @param {*} deviceTypeRef
* @returns promise of default device type commands.
*/
async function resolveDefaultDeviceTypeCommands(
db,
endpointTypeId,
deviceTypeRef
) {
let commands = await queryDeviceType.selectDeviceTypeCommandsByDeviceTypeRef(
db,
deviceTypeRef
)
return Promise.all(
commands.map((cmd) => resolveCommandState(db, endpointTypeId, cmd))
)
}
/**
* Resolve non optional commands for given parameters.
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusters
* @param {*} packageIds
* @returns Promise of clusters with commands resolved
*/
async function resolveNonOptionalCommands(
db,
endpointTypeId,
clusters,
packageIds
) {
let clustersPromises = clusters.map((cluster) =>
queryCommand
.selectCommandsByClusterId(db, cluster.clusterRef, packageIds)
.then((commands) =>
Promise.all(
commands.map((command) => {
if (!command.isOptional) {
let isOutgoing =
(cluster.side == dbEnum.side.client &&
command.source == dbEnum.source.client) ||
(cluster.side == dbEnum.side.server &&
command.source == dbEnum.source.server)
return insertOrUpdateCommandState(
db,
endpointTypeId,
command.clusterRef,
command.source,
command.id,
true,
!isOutgoing
)
} else {
return Promise.resolve()
}
})
)
)
)
return Promise.all(clustersPromises)
}
/**
* Resolve attribute defaults for endpoint type clusters.
* @param {*} db
* @param {*} endpointTypeId
* @param {*} packageId
* @param {*} endpointClusters
* @returns Array of promises for endpointClusters with attributes
*/
async function resolveDefaultAttributes(
db,
endpointTypeId,
packageId,
endpointClusters
) {
let endpointClustersPromises = endpointClusters.map((cluster) =>
queryZcl
.selectAttributesByClusterIdAndSideIncludingGlobal(
db,
cluster.clusterRef,
[packageId],
cluster.side
)
.then((attributes) => {
let promiseArray = []
promiseArray.push(
resolveNonOptionalAndReportableAttributes(
db,
endpointTypeId,
attributes,
cluster
)
)
return Promise.all(promiseArray)
})
)
return Promise.all(endpointClustersPromises)
}
/**
* Get Non optional and reportable attributes foor the given arguments.
* @param {*} db
* @param {*} endpointTypeId
* @param {*} attributes
* @param {*} cluster
* @returns Promise of Attributes
*/
async function resolveNonOptionalAndReportableAttributes(
db,
endpointTypeId,
attributes,
cluster
) {
let promises = attributes.map((attribute) => {
let settings = []
if (
attribute.reportingPolicy == dbEnum.reportingPolicy.mandatory ||
attribute.reportingPolicy == dbEnum.reportingPolicy.suggested
)
settings.push({
key: restApi.updateKey.attributeReporting,
value: true
})
if (!attribute.isOptional) {
settings.push({
key: restApi.updateKey.attributeSelected,
value: true
})
}
if (cluster.isSingleton) {
settings.push({
key: restApi.updateKey.attributeSingleton,
value: true
})
}
let clusterRef = cluster.clusterRef
if (settings.length > 0 && clusterRef != null) {
return insertOrUpdateAttributeState(
db,
endpointTypeId,
clusterRef,
attribute.side,
attribute.id,
settings,
attribute.reportMinInterval,
attribute.reportMaxInterval,
attribute.reportableChange
)
} else {
return Promise.resolve()
}
})
return Promise.all(promises)
}
/**
* Resolves into the number of endpoint types for session.
*
* @param {*} db
* @param {*} sessionId
* @returns Promise that resolves into a count.
*/
async function selectEndpointTypeCount(db, sessionId) {
let x = await dbApi.dbGet(
db,
`SELECT
COUNT(ENDPOINT_TYPE_ID) AS CNT
FROM
ENDPOINT_TYPE
INNER JOIN
SESSION_PARTITION
ON
SESSION_PARTITION.SESSION_PARTITION_ID = ENDPOINT_TYPE.SESSION_PARTITION_REF
WHERE
SESSION_PARTITION.SESSION_REF = ?`,
[sessionId]
)
return x['CNT']
}
/**
* Resolves into the number of endpoint types for session.
* by cluster ID
*
* @param {*} db
* @param {*} sessionId
* @returns Promise that resolves into a count.
*/
async function selectEndpointTypeCountByCluster(
db,
sessionId,
endpointClusterId,
side
) {
let x = await dbApi.dbGet(
db,
`
SELECT
COUNT(ENDPOINT_TYPE_ID)
FROM
ENDPOINT_TYPE
INNER JOIN
SESSION_PARTITION
ON
SESSION_PARTITION.SESSION_PARTITION_ID = ENDPOINT_TYPE.SESSION_PARTITION_REF
WHERE SESSION_PARTITION.SESSION_REF = ?
AND ENDPOINT_TYPE_ID IN
(SELECT ENDPOINT_TYPE_REF
FROM ENDPOINT_TYPE_CLUSTER
WHERE CLUSTER_REF = ? AND SIDE = ? AND ENABLED = 1) `,
[sessionId, endpointClusterId, side]
)
return x['COUNT(ENDPOINT_TYPE_ID)']
}
/**
* Get or inserts default endpoint type cluster given endpoint type, cluster ref, and side.
* @param {*} db
* @param {*} endpointTypeId
* @param {*} clusterRef
* @param {*} side
*/
async function insertOrSelectDefaultEndpointTypeCluster(
db,
endpointTypeId,
clusterRef,
side
) {
await dbApi.dbInsert(
db,
`
INSERT OR IGNORE
INTO ENDPOINT_TYPE_CLUSTER (
ENDPOINT_TYPE_REF, CLUSTER_REF, SIDE, ENABLED
) VALUES ( ?, ?, ?, ? )
`,
[endpointTypeId, clusterRef, side, false]
)
let eptClusterData = await dbApi.dbGet(
db,
`
SELECT
ENDPOINT_TYPE_CLUSTER_ID,
ENDPOINT_TYPE_REF,
CLUSTER_REF,
SIDE,
ENABLED
FROM ENDPOINT_TYPE_CLUSTER
WHERE ENDPOINT_TYPE_REF = ?
AND CLUSTER_REF = ?
AND SIDE = ?`,
[endpointTypeId, clusterRef, side]
)
return dbMapping.map.endpointTypeCluster(eptClusterData)
}
/**
* Returns a promise that resolve into an endpoint type attribute id.
*
* @param {*} db
* @param {*} sessionId
* @param {*} clusterCode
* @param {*} attributeCode
* @param {*} attributeSide
* @param {*} mfgCode
* @returns endpointType attribute id or null
*/
async function selectEndpointTypeAttributeId(
db,
endpointTypeId,
packageId,
clusterCode,
attributeCode,
attributeSide,
mfgCode
) {
let args = [
endpointTypeId,
packageId,
clusterCode,
attributeCode,
attributeSide
]
if (!(mfgCode == 0 || mfgCode == null)) args.push(mfgCode)
let rows = await dbApi.dbAll(
db,
`
SELECT
ENDPOINT_TYPE_ATTRIBUTE_ID
FROM
ENDPOINT_TYPE_CLUSTER AS ETC
INNER JOIN
ENDPOINT_TYPE_ATTRIBUTE AS ETA
ON
ETA.ENDPOINT_TYPE_CLUSTER_REF = ETC.ENDPOINT_TYPE_CLUSTER_ID
INNER JOIN
ATTRIBUTE AS A
ON
ETA.ATTRIBUTE_REF = A.ATTRIBUTE_ID
INNER JOIN
CLUSTER AS C
ON
C.CLUSTER_ID = A.CLUSTER_REF
WHERE
ETC.ENDPOINT_TYPE_REF = ?
AND C.PACKAGE_REF = ?
AND C.CODE = ?
AND A.CODE = ?
AND A.SIDE = ?
AND ${
mfgCode == 0 || mfgCode == null
? 'A.MANUFACTURER_CODE IS NULL'
: 'A.MANUFACTURER_CODE = ?'
}
`,
args
)
if (rows.length == 0) {
return null
} else if (rows.length == 1) {
return rows[0].ENDPOINT_TYPE_ATTRIBUTE_ID
} else {
throw new Error(
`Ambiguity: multiple attributes with same data loaded: ${endpointTypeId} / ${clusterCode} / ${attributeCode} / ${attributeSide}.`
)
}
}
/**
* Retrieves all the attribute data for the session.
*
* @param {*} db
* @param {*} sessionId
*/
async function selectAllSessionAttributes(db, sessionId) {
return dbApi
.dbAll(
db,
`
SELECT
A.NAME,
A.CODE AS ATTRIBUTE_CODE,
C.CODE AS CLUSTER_CODE,
ETA.DEFAULT_VALUE,
ETA.STORAGE_OPTION,
ETA.SINGLETON,
ETA.BOUNDED,
A.TYPE,
A.SIDE,
A.MIN,
A.MAX,
A.IS_WRITABLE,
A.ARRAY_TYPE,
ETA.INCLUDED_REPORTABLE,
ETA.MIN_INTERVAL,
ETA.MAX_INTERVAL,
ETA.REPORTABLE_CHANGE,
ETA.ENDPOINT_TYPE_ATTRIBUTE_ID
FROM
CLUSTER AS C
JOIN
ATTRIBUTE AS A ON A.CLUSTER_REF = C.CLUSTER_ID
JOIN
ENDPOINT_TYPE_ATTRIBUTE AS ETA ON ETA.ATTRIBUTE_REF = A.ATTRIBUTE_ID
JOIN
ENDPOINT_TYPE_CLUSTER AS ETC ON ETA.ENDPOINT_TYPE_CLUSTER_REF = ETC.ENDPOINT_TYPE_CLUSTER_ID
JOIN
ENDPOINT_TYPE AS ET ON ETC.ENDPOINT_TYPE_REF = ET.ENDPOINT_TYPE_ID
INNER JOIN
SESSION_PARTITION
ON
ET.SESSION_PARTITION_REF = SESSION_PARTITION.SESSION_PARTITION_ID
WHERE
SESSION_PARTITION.SESSION_REF = ? AND ETA.INCLUDED = 1
ORDER BY
CLUSTER_CODE, ATTRIBUTE_CODE
`,
[sessionId]
)
.then((rows) =>
rows.map((row) => {
return {
endpointTypeAttributeId: row.ENDPOINT_TYPE_ATTRIBUTE_ID,
name: row.NAME,
attributeCode: row.ATTRIBUTE_CODE,
clusterCode: row.CLUSTER_CODE,
defaultValue: row.DEFAULT_VALUE,
storageOption: row.STORAGE_OPTION,
isSingleton: row.SINGLETON,
isBounded: row.BOUNDED,
type: row.TYPE,
side: row.SIDE,
min: row.MIN,
max: row.MAX,
writable: row.IS_WRITABLE,
entryType: row.ARRAY_TYPE,
reportable: {
included: row.INCLUDED_REPORTABLE,
minInterval: row.MIN_INTERVAL,
maxInterval: row.MAX_INTERVAL,
change: row.REPORTABLE_CHANGE
}
}
})
)
}
/**
* Sets a given cluster to be included on a given endpoint.
*
* @param {*} db
* @param {*} packageId
* @param {*} clusterCode
* @param {*} isIncluded
* @param {*} side
*/
async function setClusterIncluded(
db,
packageId,
endpointTypeId,
clusterCode,
isIncluded,
side
) {
let cluster = await queryZcl.selectClusterByCode(db, packageId, clusterCode)
let clusterState = await selectClusterState(
db,
endpointTypeId,
cluster.id,
side
)
let insertDefaults = clusterState == null
await insertOrReplaceClusterState(
db,
endpointTypeId,
cluster.id,
side,
isIncluded
)
if (insertDefaults) {
await insertClusterDefaults(db, endpointTypeId, packageId, {
clusterRef: cluster.id,
side: side
})
}
}
// exports
exports.insertOrReplaceClusterState = insertOrReplaceClusterState
exports.selectClusterState = selectClusterState
exports.selectClusterStatesForAllEndpoints = selectClusterStatesForAllEndpoints
exports.insertOrUpdateAttributeState = insertOrUpdateAttributeState
exports.insertOrUpdateCommandState = insertOrUpdateCommandState
exports.insertOrUpdateEventState = insertOrUpdateEventState
exports.convertRestKeyToDbColumn = convertRestKeyToDbColumn
exports.duplicateEndpointType = duplicateEndpointType
exports.selectEndpointClusters = selectEndpointClusters
exports.updateEndpoint = updateEndpoint
exports.updateParentEndpoint = updateParentEndpoint
exports.insertEndpointType = insertEndpointType
exports.updateEndpointType = updateEndpointType
exports.selectCountOfEndpointsWithGivenEndpointIdentifier =
selectCountOfEndpointsWithGivenEndpointIdentifier
exports.selectEndpointTypeCount = selectEndpointTypeCount
exports.selectEndpointTypeCountByCluster = selectEndpointTypeCountByCluster
exports.selectAllSessionAttributes = selectAllSessionAttributes
exports.insertClusterDefaults = insertClusterDefaults
exports.setClusterIncluded = setClusterIncluded
exports.selectEndpointTypeAttributeId = selectEndpointTypeAttributeId
exports.updateEndpointTypeAttribute = updateEndpointTypeAttribute