Skip to content

Commit 43da6e8

Browse files
authored
Feature Page phase 1 (#1409)
* 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
1 parent 8f98309 commit 43da6e8

File tree

21 files changed

+653
-69
lines changed

21 files changed

+653
-69
lines changed

cypress/e2e/clusters/cluster-multiple-search.cy.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ describe('Add multiple clusters and search', () => {
1515
cy.fixture('data').then((data) => {
1616
cy.addEndpoint(data.endpoint4, data.cluster1)
1717
})
18-
cy.get('.flex > strong').should('contain', 'Endpoint - 1')
18+
cy.get('.flex > strong').should('contain', '#1')
1919
cy.fixture('data').then((data) => {
2020
cy.addEndpoint(data.endpoint3, data.cluster1)
2121
})
22-
cy.get('.flex > strong').should('contain', 'Endpoint - 2')
22+
cy.get('.flex > strong').should('contain', '#2')
2323
cy.fixture('data').then((data) => {
2424
cy.get('tbody')
2525
.children()
2626
.should('contain', data.cluster5)
2727
.and('contain', data.cluster6)
2828
cy.addEndpoint(data.endpoint5, data.cluster1)
2929
})
30-
cy.get('.flex > strong').should('contain', 'Endpoint - 3')
30+
cy.get('.flex > strong').should('contain', '#3')
3131
cy.fixture('data').then((data) => {
3232
cy.get('#General > .q-expansion-item__container > .q-item').click({
3333
force: true

src-electron/db/db-mapping.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,23 @@ exports.map = {
245245
}
246246
},
247247

248+
deviceTypeFeature: (x) => {
249+
if (x == null) return undefined
250+
return {
251+
deviceType: x.DEVICE_TYPE_NAME,
252+
cluster: x.CLUSTER_NAME,
253+
includeServer: x.INCLUDE_SERVER,
254+
includeClient: x.INCLUDE_CLIENT,
255+
conformance: x.DEVICE_TYPE_CLUSTER_CONFORMANCE,
256+
id: x.FEATURE_ID,
257+
name: x.FEATURE_NAME,
258+
code: x.CODE,
259+
bit: x.BIT,
260+
default_value: x.DEFAULT_VALUE,
261+
description: x.DESCRIPTION
262+
}
263+
},
264+
248265
domain: (x) => {
249266
if (x == null) return undefined
250267
return {

src-electron/db/query-feature.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
*
3+
* Copyright (c) 2021 Silicon Labs
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* This module provides queries for features.
20+
*
21+
* @module DB API: feature related queries
22+
*/
23+
const dbApi = require('./db-api.js')
24+
const dbMapping = require('./db-mapping.js')
25+
26+
/**
27+
* Get all device type features associated with a list of device type refs
28+
* @param {*} db
29+
* @param {*} deviceTypeRefs
30+
* @returns All feature information and device type conformance
31+
* with associated device type and cluster details
32+
*/
33+
async function getFeaturesByDeviceTypeRefs(db, deviceTypeRefs) {
34+
let deviceTypeRefsSql = deviceTypeRefs.map(() => '?').join(', ')
35+
let features = await dbApi.dbAll(
36+
db,
37+
`
38+
SELECT
39+
d.DESCRIPTION AS DEVICE_TYPE_NAME,
40+
dc.CLUSTER_NAME,
41+
dc.INCLUDE_SERVER,
42+
dc.INCLUDE_CLIENT,
43+
df.DEVICE_TYPE_CLUSTER_CONFORMANCE,
44+
f.FEATURE_ID,
45+
f.NAME AS FEATURE_NAME,
46+
f.CODE,
47+
f.BIT,
48+
f.DEFAULT_VALUE,
49+
f.DESCRIPTION
50+
FROM
51+
DEVICE_TYPE d
52+
JOIN
53+
DEVICE_TYPE_CLUSTER dc
54+
ON
55+
d.DEVICE_TYPE_ID = dc.DEVICE_TYPE_REF
56+
JOIN
57+
DEVICE_TYPE_FEATURE df
58+
ON
59+
dc.DEVICE_TYPE_CLUSTER_ID = df.DEVICE_TYPE_CLUSTER_REF
60+
JOIN
61+
FEATURE f
62+
ON
63+
df.FEATURE_REF = f.FEATURE_ID
64+
WHERE
65+
d.DEVICE_TYPE_ID IN (${deviceTypeRefsSql})
66+
ORDER BY
67+
d.DEVICE_TYPE_ID,
68+
dc.CLUSTER_REF,
69+
f.FEATURE_ID
70+
`,
71+
deviceTypeRefs
72+
)
73+
return features.map(dbMapping.map.deviceTypeFeature)
74+
}
75+
76+
exports.getFeaturesByDeviceTypeRefs = getFeaturesByDeviceTypeRefs

src-electron/db/query-loader.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,14 +1124,14 @@ async function insertDeviceTypeFeatures(db, dtClusterRefDataPairs) {
11241124
let dtClusterRef = dtClusterRefDataPair.dtClusterRef
11251125
let clusterData = dtClusterRefDataPair.clusterData
11261126
if ('features' in clusterData && clusterData.features.length > 0) {
1127-
clusterData.features.forEach((featureCode) => {
1128-
features.push([dtClusterRef, featureCode])
1127+
clusterData.features.forEach((feature) => {
1128+
features.push([dtClusterRef, feature.code, feature.conformance])
11291129
})
11301130
}
11311131
})
11321132
return dbApi.dbMultiInsert(
11331133
db,
1134-
'INSERT INTO DEVICE_TYPE_FEATURE (DEVICE_TYPE_CLUSTER_REF, FEATURE_CODE) VALUES (?, ?)',
1134+
'INSERT INTO DEVICE_TYPE_FEATURE (DEVICE_TYPE_CLUSTER_REF, FEATURE_CODE, DEVICE_TYPE_CLUSTER_CONFORMANCE) VALUES (?, ?, ?)',
11351135
features
11361136
)
11371137
}

src-electron/db/zap-schema.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ CREATE TABLE IF NOT EXISTS "DEVICE_TYPE_FEATURE" (
441441
"DEVICE_TYPE_CLUSTER_REF" integer,
442442
"FEATURE_REF" integer,
443443
"FEATURE_CODE" text,
444+
"DEVICE_TYPE_CLUSTER_CONFORMANCE" text,
444445
foreign key (DEVICE_TYPE_CLUSTER_REF) references DEVICE_TYPE_CLUSTER(DEVICE_TYPE_CLUSTER_ID) ON DELETE CASCADE ON UPDATE CASCADE,
445446
foreign key (FEATURE_REF) references FEATURE(FEATURE_ID) ON DELETE CASCADE ON UPDATE CASCADE
446447
UNIQUE(DEVICE_TYPE_CLUSTER_REF, FEATURE_REF)

src-electron/rest/user-data.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const env = require('../util/env')
2424
const queryZcl = require('../db/query-zcl.js')
2525
const queryAttribute = require('../db/query-attribute.js')
2626
const queryCommand = require('../db/query-command.js')
27+
const queryFeature = require('../db/query-feature')
2728
const queryConfig = require('../db/query-config.js')
2829
const upgrade = require('../sdk/matter.js')
2930
const querySessionNotification = require('../db/query-session-notification.js')
@@ -72,6 +73,24 @@ function httpGetEndpointIds(db) {
7273
response.status(StatusCodes.OK).json(endpointIds)
7374
}
7475
}
76+
77+
/**
78+
* HTTP GET: device type features
79+
*
80+
* @param {*} db
81+
* @returns callback for the express uri registration
82+
*/
83+
function httpGetDeviceTypeFeatures(db) {
84+
return async (request, response) => {
85+
let deviceTypeRefs = request.query.deviceTypeRefs
86+
let deviceTypeFeatures = await queryFeature.getFeaturesByDeviceTypeRefs(
87+
db,
88+
deviceTypeRefs
89+
)
90+
response.status(StatusCodes.OK).json(deviceTypeFeatures)
91+
}
92+
}
93+
7594
/**
7695
* HTTP GET: session get notifications
7796
*
@@ -1030,6 +1049,10 @@ exports.get = [
10301049
uri: restApi.uri.getAllSessionKeyValues,
10311050
callback: httpGetSessionKeyValues
10321051
},
1052+
{
1053+
uri: restApi.uri.deviceTypeFeatures,
1054+
callback: httpGetDeviceTypeFeatures
1055+
},
10331056
{
10341057
uri: restApi.uri.sessionNotification,
10351058
callback: httpGetSessionNotifications

src-electron/zcl/zcl-loader-silabs.js

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ function prepareCluster(cluster, context, isExtension = false) {
686686
bit: feature.$.bit,
687687
defaultValue: feature.$.default,
688688
description: feature.$.summary,
689-
conformance: feature.$.conformance
689+
conformance: parseFeatureConformance(feature)
690690
}
691691

692692
ret.features.push(f)
@@ -1638,10 +1638,11 @@ function prepareDeviceType(deviceType) {
16381638
}
16391639
if ('features' in include) {
16401640
include.features[0].feature.forEach((f) => {
1641-
// Only adding madatory features for now
1642-
if (f.mandatoryConform && f.mandatoryConform[0] === '') {
1643-
features.push(f.$.name)
1644-
}
1641+
let conformance = parseFeatureConformance(f)
1642+
features.push({
1643+
code: f.$.code,
1644+
conformance: conformance
1645+
})
16451646
})
16461647
}
16471648
ret.clusters.push({
@@ -2202,6 +2203,114 @@ async function parseFeatureFlags(db, packageId, featureFlags) {
22022203
)
22032204
}
22042205

2206+
/**
2207+
* Parses feature conformance or an operand in feature conformance recursively from xml data.
2208+
*
2209+
* An example of parsing the conformance of 'User' device type feature:
2210+
*
2211+
* Input operand from xml data:
2212+
* {
2213+
* "$": {"code": "USR", "name": "User"},
2214+
* "mandatoryConform": [
2215+
* { "andTerm": [
2216+
* {
2217+
* "condition": [{"$": {"name": "Matter"}}],
2218+
* "orTerm": [
2219+
* { "feature": [
2220+
* { "$": {"name": "PIN"}},
2221+
* { "$": {"name": "RID"}},
2222+
* { "$": {"name": "FPG"}},
2223+
* { "$": {"name": "FACE"}}
2224+
* ]
2225+
* }
2226+
* ]
2227+
* }
2228+
* ]
2229+
* }
2230+
* ]
2231+
* }
2232+
*
2233+
* Output device type feature conformance string:
2234+
* "Matter & (PIN | RID | FPG | FACE)"
2235+
*
2236+
* @param {*} operand - The operand to be parsed.
2237+
* @returns The feature conformance string.
2238+
*/
2239+
function parseFeatureConformance(operand) {
2240+
if (operand.mandatoryConform) {
2241+
let insideTerm = operand.mandatoryConform[0]
2242+
// Recurse further if insideTerm is not empty
2243+
if (insideTerm && Object.keys(insideTerm).toString() != '$') {
2244+
return parseFeatureConformance(operand.mandatoryConform[0])
2245+
} else {
2246+
return 'M'
2247+
}
2248+
} else if (operand.optionalConform) {
2249+
let insideTerm = operand.optionalConform[0]
2250+
// check '$' key is not the only key in the object to handle special cases
2251+
// e.g. '<optionalConform choice="a" more="true"/>'
2252+
if (insideTerm && Object.keys(insideTerm).toString() != '$') {
2253+
return `[${parseFeatureConformance(operand.optionalConform[0])}]`
2254+
} else {
2255+
return 'O'
2256+
}
2257+
} else if (operand.provisionalConform) {
2258+
return 'P'
2259+
} else if (operand.disallowConform) {
2260+
return 'X'
2261+
} else if (operand.deprecateConform) {
2262+
return 'D'
2263+
} else if (operand.feature) {
2264+
return operand.feature[0].$.name
2265+
} else if (operand.condition) {
2266+
return operand.condition[0].$.name
2267+
} else if (operand.otherwiseConform) {
2268+
return Object.entries(operand.otherwiseConform[0])
2269+
.map(([key, value]) => parseFeatureConformance({ [key]: value }))
2270+
.join(', ')
2271+
} else if (operand.notTerm) {
2272+
let notTerms = parseFeatureConformance(operand.notTerm[0])
2273+
// need to surround notTerms with '()' if it contains multiple terms
2274+
// e.g. !(A | B) or !(A & B)
2275+
return notTerms.includes('&') || notTerms.includes('|')
2276+
? `!(${notTerms})`
2277+
: `!${notTerms}`
2278+
} else if (operand.andTerm) {
2279+
return parseAndOrConformanceTerms(operand.andTerm, '&')
2280+
} else if (operand.orTerm) {
2281+
return parseAndOrConformanceTerms(operand.orTerm, '|')
2282+
} else {
2283+
return ''
2284+
}
2285+
}
2286+
2287+
/**
2288+
* Helper function to parse andTerm or orTerm from xml data
2289+
* @param {*} operand
2290+
* @param {*} joinChar
2291+
* @returns feature conformance string
2292+
*/
2293+
function parseAndOrConformanceTerms(operand, joinChar) {
2294+
// when joining multiple orTerms inside andTerms, we need to
2295+
// surround them with '()', vice versa for andTerms inside orTerms
2296+
// e.g. A & (B | C) or A | (B & C)
2297+
let oppositeChar = joinChar === '&' ? '|' : '&'
2298+
let oppositeTerm = joinChar === '&' ? 'orTerm' : 'andTerm'
2299+
2300+
return Object.entries(operand[0])
2301+
.map(([key, value]) => {
2302+
if (key == 'feature' || key == 'condition') {
2303+
return value.map((operand) => operand.$.name).join(` ${joinChar} `)
2304+
} else if (key == oppositeTerm) {
2305+
let terms = parseFeatureConformance({ [key]: value })
2306+
return terms.includes(oppositeChar) ? `(${terms})` : terms
2307+
} else {
2308+
return ''
2309+
}
2310+
})
2311+
.join(` ${joinChar} `)
2312+
}
2313+
22052314
/**
22062315
* Inside the `zcl.json` can be a `featureFlags` key, which is
22072316
* a general purpose object. It contains keys, that map to objects.

src-shared/db-enum.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,26 @@ exports.packageMatch = {
208208
strict: 'strict', // This mechanism will ONLY use the records of packages in the .zap file.
209209
ignore: 'ignore' // This mechanism will completely ignore the use of packages in the .zap file.
210210
}
211+
212+
exports.deviceTypeFeature = {
213+
name: {
214+
deviceType: 'deviceType',
215+
cluster: 'cluster',
216+
clusterSide: 'clusterSide',
217+
featureName: 'featureName',
218+
code: 'code',
219+
conformance: 'conformance',
220+
bit: 'bit',
221+
description: 'description'
222+
},
223+
label: {
224+
deviceType: 'Device Type',
225+
cluster: 'Cluster',
226+
clusterSide: 'Cluster Side',
227+
featureName: 'Feature Name',
228+
code: 'Code',
229+
conformance: 'Conformance',
230+
bit: 'Bit',
231+
description: 'Description'
232+
}
233+
}

src-shared/rest-api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const uri = {
6767
deviceTypeAttributes: '/zcl/deviceTypeAttributes/',
6868
deviceTypeCommands: '/zcl/deviceTypeCommands/',
6969
deviceTypeEvents: '/zcl/deviceTypeEvents/',
70+
deviceTypeFeatures: '/zcl/deviceTypeFeatures/',
7071
sessionAttempt: '/zcl/sessionAttempt',
7172
initializeSession: '/zcl/initializeSession',
7273
sessionCreate: '/zcl/sessionCreate',

0 commit comments

Comments
 (0)