Skip to content

Commit

Permalink
Feature Page phase 1 (project-chip#1409)
Browse files Browse the repository at this point in the history
* add conformance column

* parse xml data

* feature page v1

* add feature state

* add cluster side

* resolve comments

* add unit test

* improve parseConformance function

* change button location & improve UI

* restructure ui-options

* fix Cypress test
  • Loading branch information
ethanzhouyc authored Aug 27, 2024
1 parent 8f98309 commit 43da6e8
Show file tree
Hide file tree
Showing 21 changed files with 653 additions and 69 deletions.
6 changes: 3 additions & 3 deletions cypress/e2e/clusters/cluster-multiple-search.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ describe('Add multiple clusters and search', () => {
cy.fixture('data').then((data) => {
cy.addEndpoint(data.endpoint4, data.cluster1)
})
cy.get('.flex > strong').should('contain', 'Endpoint - 1')
cy.get('.flex > strong').should('contain', '#1')
cy.fixture('data').then((data) => {
cy.addEndpoint(data.endpoint3, data.cluster1)
})
cy.get('.flex > strong').should('contain', 'Endpoint - 2')
cy.get('.flex > strong').should('contain', '#2')
cy.fixture('data').then((data) => {
cy.get('tbody')
.children()
.should('contain', data.cluster5)
.and('contain', data.cluster6)
cy.addEndpoint(data.endpoint5, data.cluster1)
})
cy.get('.flex > strong').should('contain', 'Endpoint - 3')
cy.get('.flex > strong').should('contain', '#3')
cy.fixture('data').then((data) => {
cy.get('#General > .q-expansion-item__container > .q-item').click({
force: true
Expand Down
17 changes: 17 additions & 0 deletions src-electron/db/db-mapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,23 @@ exports.map = {
}
},

deviceTypeFeature: (x) => {
if (x == null) return undefined
return {
deviceType: x.DEVICE_TYPE_NAME,
cluster: x.CLUSTER_NAME,
includeServer: x.INCLUDE_SERVER,
includeClient: x.INCLUDE_CLIENT,
conformance: x.DEVICE_TYPE_CLUSTER_CONFORMANCE,
id: x.FEATURE_ID,
name: x.FEATURE_NAME,
code: x.CODE,
bit: x.BIT,
default_value: x.DEFAULT_VALUE,
description: x.DESCRIPTION
}
},

domain: (x) => {
if (x == null) return undefined
return {
Expand Down
76 changes: 76 additions & 0 deletions src-electron/db/query-feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
*
* Copyright (c) 2021 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 features.
*
* @module DB API: feature related queries
*/
const dbApi = require('./db-api.js')
const dbMapping = require('./db-mapping.js')

/**
* Get all device type features associated with a list of device type refs
* @param {*} db
* @param {*} deviceTypeRefs
* @returns All feature information and device type conformance
* with associated device type and cluster details
*/
async function getFeaturesByDeviceTypeRefs(db, deviceTypeRefs) {
let deviceTypeRefsSql = deviceTypeRefs.map(() => '?').join(', ')
let features = await dbApi.dbAll(
db,
`
SELECT
d.DESCRIPTION AS DEVICE_TYPE_NAME,
dc.CLUSTER_NAME,
dc.INCLUDE_SERVER,
dc.INCLUDE_CLIENT,
df.DEVICE_TYPE_CLUSTER_CONFORMANCE,
f.FEATURE_ID,
f.NAME AS FEATURE_NAME,
f.CODE,
f.BIT,
f.DEFAULT_VALUE,
f.DESCRIPTION
FROM
DEVICE_TYPE d
JOIN
DEVICE_TYPE_CLUSTER dc
ON
d.DEVICE_TYPE_ID = dc.DEVICE_TYPE_REF
JOIN
DEVICE_TYPE_FEATURE df
ON
dc.DEVICE_TYPE_CLUSTER_ID = df.DEVICE_TYPE_CLUSTER_REF
JOIN
FEATURE f
ON
df.FEATURE_REF = f.FEATURE_ID
WHERE
d.DEVICE_TYPE_ID IN (${deviceTypeRefsSql})
ORDER BY
d.DEVICE_TYPE_ID,
dc.CLUSTER_REF,
f.FEATURE_ID
`,
deviceTypeRefs
)
return features.map(dbMapping.map.deviceTypeFeature)
}

exports.getFeaturesByDeviceTypeRefs = getFeaturesByDeviceTypeRefs
6 changes: 3 additions & 3 deletions src-electron/db/query-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1124,14 +1124,14 @@ async function insertDeviceTypeFeatures(db, dtClusterRefDataPairs) {
let dtClusterRef = dtClusterRefDataPair.dtClusterRef
let clusterData = dtClusterRefDataPair.clusterData
if ('features' in clusterData && clusterData.features.length > 0) {
clusterData.features.forEach((featureCode) => {
features.push([dtClusterRef, featureCode])
clusterData.features.forEach((feature) => {
features.push([dtClusterRef, feature.code, feature.conformance])
})
}
})
return dbApi.dbMultiInsert(
db,
'INSERT INTO DEVICE_TYPE_FEATURE (DEVICE_TYPE_CLUSTER_REF, FEATURE_CODE) VALUES (?, ?)',
'INSERT INTO DEVICE_TYPE_FEATURE (DEVICE_TYPE_CLUSTER_REF, FEATURE_CODE, DEVICE_TYPE_CLUSTER_CONFORMANCE) VALUES (?, ?, ?)',
features
)
}
Expand Down
1 change: 1 addition & 0 deletions src-electron/db/zap-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ CREATE TABLE IF NOT EXISTS "DEVICE_TYPE_FEATURE" (
"DEVICE_TYPE_CLUSTER_REF" integer,
"FEATURE_REF" integer,
"FEATURE_CODE" text,
"DEVICE_TYPE_CLUSTER_CONFORMANCE" text,
foreign key (DEVICE_TYPE_CLUSTER_REF) references DEVICE_TYPE_CLUSTER(DEVICE_TYPE_CLUSTER_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (FEATURE_REF) references FEATURE(FEATURE_ID) ON DELETE CASCADE ON UPDATE CASCADE
UNIQUE(DEVICE_TYPE_CLUSTER_REF, FEATURE_REF)
Expand Down
23 changes: 23 additions & 0 deletions src-electron/rest/user-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const env = require('../util/env')
const queryZcl = require('../db/query-zcl.js')
const queryAttribute = require('../db/query-attribute.js')
const queryCommand = require('../db/query-command.js')
const queryFeature = require('../db/query-feature')
const queryConfig = require('../db/query-config.js')
const upgrade = require('../sdk/matter.js')
const querySessionNotification = require('../db/query-session-notification.js')
Expand Down Expand Up @@ -72,6 +73,24 @@ function httpGetEndpointIds(db) {
response.status(StatusCodes.OK).json(endpointIds)
}
}

/**
* HTTP GET: device type features
*
* @param {*} db
* @returns callback for the express uri registration
*/
function httpGetDeviceTypeFeatures(db) {
return async (request, response) => {
let deviceTypeRefs = request.query.deviceTypeRefs
let deviceTypeFeatures = await queryFeature.getFeaturesByDeviceTypeRefs(
db,
deviceTypeRefs
)
response.status(StatusCodes.OK).json(deviceTypeFeatures)
}
}

/**
* HTTP GET: session get notifications
*
Expand Down Expand Up @@ -1030,6 +1049,10 @@ exports.get = [
uri: restApi.uri.getAllSessionKeyValues,
callback: httpGetSessionKeyValues
},
{
uri: restApi.uri.deviceTypeFeatures,
callback: httpGetDeviceTypeFeatures
},
{
uri: restApi.uri.sessionNotification,
callback: httpGetSessionNotifications
Expand Down
119 changes: 114 additions & 5 deletions src-electron/zcl/zcl-loader-silabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ function prepareCluster(cluster, context, isExtension = false) {
bit: feature.$.bit,
defaultValue: feature.$.default,
description: feature.$.summary,
conformance: feature.$.conformance
conformance: parseFeatureConformance(feature)
}

ret.features.push(f)
Expand Down Expand Up @@ -1638,10 +1638,11 @@ function prepareDeviceType(deviceType) {
}
if ('features' in include) {
include.features[0].feature.forEach((f) => {
// Only adding madatory features for now
if (f.mandatoryConform && f.mandatoryConform[0] === '') {
features.push(f.$.name)
}
let conformance = parseFeatureConformance(f)
features.push({
code: f.$.code,
conformance: conformance
})
})
}
ret.clusters.push({
Expand Down Expand Up @@ -2202,6 +2203,114 @@ async function parseFeatureFlags(db, packageId, featureFlags) {
)
}

/**
* Parses feature conformance or an operand in feature conformance recursively from xml data.
*
* An example of parsing the conformance of 'User' device type feature:
*
* Input operand from xml data:
* {
* "$": {"code": "USR", "name": "User"},
* "mandatoryConform": [
* { "andTerm": [
* {
* "condition": [{"$": {"name": "Matter"}}],
* "orTerm": [
* { "feature": [
* { "$": {"name": "PIN"}},
* { "$": {"name": "RID"}},
* { "$": {"name": "FPG"}},
* { "$": {"name": "FACE"}}
* ]
* }
* ]
* }
* ]
* }
* ]
* }
*
* Output device type feature conformance string:
* "Matter & (PIN | RID | FPG | FACE)"
*
* @param {*} operand - The operand to be parsed.
* @returns The feature conformance string.
*/
function parseFeatureConformance(operand) {
if (operand.mandatoryConform) {
let insideTerm = operand.mandatoryConform[0]
// Recurse further if insideTerm is not empty
if (insideTerm && Object.keys(insideTerm).toString() != '$') {
return parseFeatureConformance(operand.mandatoryConform[0])
} else {
return 'M'
}
} else if (operand.optionalConform) {
let insideTerm = operand.optionalConform[0]
// check '$' key is not the only key in the object to handle special cases
// e.g. '<optionalConform choice="a" more="true"/>'
if (insideTerm && Object.keys(insideTerm).toString() != '$') {
return `[${parseFeatureConformance(operand.optionalConform[0])}]`
} else {
return 'O'
}
} else if (operand.provisionalConform) {
return 'P'
} else if (operand.disallowConform) {
return 'X'
} else if (operand.deprecateConform) {
return 'D'
} else if (operand.feature) {
return operand.feature[0].$.name
} else if (operand.condition) {
return operand.condition[0].$.name
} else if (operand.otherwiseConform) {
return Object.entries(operand.otherwiseConform[0])
.map(([key, value]) => parseFeatureConformance({ [key]: value }))
.join(', ')
} else if (operand.notTerm) {
let notTerms = parseFeatureConformance(operand.notTerm[0])
// need to surround notTerms with '()' if it contains multiple terms
// e.g. !(A | B) or !(A & B)
return notTerms.includes('&') || notTerms.includes('|')
? `!(${notTerms})`
: `!${notTerms}`
} else if (operand.andTerm) {
return parseAndOrConformanceTerms(operand.andTerm, '&')
} else if (operand.orTerm) {
return parseAndOrConformanceTerms(operand.orTerm, '|')
} else {
return ''
}
}

/**
* Helper function to parse andTerm or orTerm from xml data
* @param {*} operand
* @param {*} joinChar
* @returns feature conformance string
*/
function parseAndOrConformanceTerms(operand, joinChar) {
// when joining multiple orTerms inside andTerms, we need to
// surround them with '()', vice versa for andTerms inside orTerms
// e.g. A & (B | C) or A | (B & C)
let oppositeChar = joinChar === '&' ? '|' : '&'
let oppositeTerm = joinChar === '&' ? 'orTerm' : 'andTerm'

return Object.entries(operand[0])
.map(([key, value]) => {
if (key == 'feature' || key == 'condition') {
return value.map((operand) => operand.$.name).join(` ${joinChar} `)
} else if (key == oppositeTerm) {
let terms = parseFeatureConformance({ [key]: value })
return terms.includes(oppositeChar) ? `(${terms})` : terms
} else {
return ''
}
})
.join(` ${joinChar} `)
}

/**
* Inside the `zcl.json` can be a `featureFlags` key, which is
* a general purpose object. It contains keys, that map to objects.
Expand Down
23 changes: 23 additions & 0 deletions src-shared/db-enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,26 @@ exports.packageMatch = {
strict: 'strict', // This mechanism will ONLY use the records of packages in the .zap file.
ignore: 'ignore' // This mechanism will completely ignore the use of packages in the .zap file.
}

exports.deviceTypeFeature = {
name: {
deviceType: 'deviceType',
cluster: 'cluster',
clusterSide: 'clusterSide',
featureName: 'featureName',
code: 'code',
conformance: 'conformance',
bit: 'bit',
description: 'description'
},
label: {
deviceType: 'Device Type',
cluster: 'Cluster',
clusterSide: 'Cluster Side',
featureName: 'Feature Name',
code: 'Code',
conformance: 'Conformance',
bit: 'Bit',
description: 'Description'
}
}
1 change: 1 addition & 0 deletions src-shared/rest-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const uri = {
deviceTypeAttributes: '/zcl/deviceTypeAttributes/',
deviceTypeCommands: '/zcl/deviceTypeCommands/',
deviceTypeEvents: '/zcl/deviceTypeEvents/',
deviceTypeFeatures: '/zcl/deviceTypeFeatures/',
sessionAttempt: '/zcl/sessionAttempt',
initializeSession: '/zcl/initializeSession',
sessionCreate: '/zcl/sessionCreate',
Expand Down
Loading

0 comments on commit 43da6e8

Please sign in to comment.