Skip to content

Commit dbef348

Browse files
authored
Creating extensions for ZAP files using .zapExtension files (#1552)
- .zapExtension files can now be used to extend the .zap files to add additional clusters, commands or attributes on an endoint - Adding the logic to do this during generation or during file open using command line arguments. - Adding unit tests - Adding documentation on usage - Added an attribute to the xml so updated one of the existing tests - Fixing existing tests and adding tests for zap extensions with clusters and attributes on an endpoint type id - Linking the extension file to the endpoint identifiers and not endpoint type ids - Github: ZAP#1254
1 parent ce14d1a commit dbef348

File tree

18 files changed

+5389
-23
lines changed

18 files changed

+5389
-23
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ This software is licensed under [Apache 2.0 license](LICENSE.txt).
3939

4040
- [ZAP Template Helpers](docs/helpers.md)
4141
- [ZAP External Template Helpers](docs/external-helpers.md)
42+
- [ZAP file Extensions](docs/zap-file-extensions.md)
4243
- [FAQ/Developer dependencies](docs/faq.md)
4344
- [Release instructions](docs/release.md)
4445
- [Development Instructions](docs/development-instructions.md)

docs/api.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16929,11 +16929,11 @@ Utility module for ZAP UI
1692916929

1693016930
* [JS API: Utility module for ZAP UI](#module_JS API_ Utility module for ZAP UI)
1693116931
* [~showErrorMessage(title, err)](#module_JS API_ Utility module for ZAP UI..showErrorMessage)
16932-
* [~openFileConfiguration(db, filePath, httpPort)](#module_JS API_ Utility module for ZAP UI..openFileConfiguration)
16932+
* [~openFileConfiguration(db, filePath, httpPort, zapFileExtensions)](#module_JS API_ Utility module for ZAP UI..openFileConfiguration)
1693316933
* [~openNewConfiguration(httpPort, options:)](#module_JS API_ Utility module for ZAP UI..openNewConfiguration)
1693416934
* [~toggleDirtyFlag(browserWindow, dirty)](#module_JS API_ Utility module for ZAP UI..toggleDirtyFlag)
1693516935
* [~openFileDialogAndReportResult(browserWindow, options)](#module_JS API_ Utility module for ZAP UI..openFileDialogAndReportResult)
16936-
* [~enableUi(port, zapFiles, uiMode, standalone)](#module_JS API_ Utility module for ZAP UI..enableUi) ⇒
16936+
* [~enableUi(port, zapFiles, uiMode, standalone, zapFileExtensions)](#module_JS API_ Utility module for ZAP UI..enableUi) ⇒
1693716937

1693816938
<a name="module_JS API_ Utility module for ZAP UI..showErrorMessage"></a>
1693916939

@@ -16949,7 +16949,7 @@ Simple dialog to show error messages from electron renderer scope.
1694916949

1695016950
<a name="module_JS API_ Utility module for ZAP UI..openFileConfiguration"></a>
1695116951

16952-
### JS API: Utility module for ZAP UI~openFileConfiguration(db, filePath, httpPort)
16952+
### JS API: Utility module for ZAP UI~openFileConfiguration(db, filePath, httpPort, zapFileExtensions)
1695316953
Process a single file, parsing it in as JSON and then possibly opening
1695416954
a new window if all is good.
1695516955

@@ -16960,6 +16960,7 @@ a new window if all is good.
1696016960
| db | <code>\*</code> | |
1696116961
| filePath | <code>\*</code> | |
1696216962
| httpPort | <code>\*</code> | Server port for the URL that will be constructed. |
16963+
| zapFileExtensions | <code>\*</code> | Extend a zap file with zapExtension |
1696316964

1696416965
<a name="module_JS API_ Utility module for ZAP UI..openNewConfiguration"></a>
1696516966

@@ -17001,7 +17002,7 @@ reports result back through the API.
1700117002

1700217003
<a name="module_JS API_ Utility module for ZAP UI..enableUi"></a>
1700317004

17004-
### JS API: Utility module for ZAP UI~enableUi(port, zapFiles, uiMode, standalone) ⇒
17005+
### JS API: Utility module for ZAP UI~enableUi(port, zapFiles, uiMode, standalone, zapFileExtensions) ⇒
1700517006
Enable the UI open using the given arguments.
1700617007

1700717008
**Kind**: inner method of [<code>JS API: Utility module for ZAP UI</code>](#module_JS API_ Utility module for ZAP UI)
@@ -17013,6 +17014,7 @@ Enable the UI open using the given arguments.
1701317014
| zapFiles | <code>\*</code> |
1701417015
| uiMode | <code>\*</code> |
1701517016
| standalone | <code>\*</code> |
17017+
| zapFileExtensions | <code>\*</code> |
1701617018

1701717019
<a name="module_JS API_ Window module for ZAP UI"></a>
1701817020

docs/zap-file-extensions.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# ZAP File Extensions
2+
3+
## Overview
4+
5+
ZAP file extensions allow users to extend the functionality of existing ZAP configuration files(.zap) by merging additional data from extension files(.zapExtension). This feature is particularly useful for adding new clusters, attributes, or other configuration elements to an existing endpoint identifier without modifying the base ZAP file. Note that if the extension element is already present in the base configuration then it will not be modified. Also note that if you open a .zap baseline file along with a .zapExtension extension file and then save the configuration then this will always produce a single .zap file which includes the extension configuration.
6+
7+
## How It Works
8+
9+
1. **Base ZAP File(.zap)**: The primary configuration file containing the initial setup. eg [zapFile](../test/resource/lighting-matter.zap)
10+
2. **Extension ZAP File(.zapExtension)**: A supplementary file that adds to elements in the base file. eg [zapFileExtension](../test/resource/zapExtension1.zapExtension). This file shows how everything is wrapped within endpoints object and linked to the endpoint identifier.
11+
3. **Merging Process**: During the import process, the extension file's content is merged into the base file content. However, if you save this imported file along with its extension then this saves the content in one explicit file.
12+
13+
## Usage
14+
15+
### Generation
16+
17+
[ZAP executable] generate --noUi --noServer -o [output directory path] --packageMatch fuzzy --zcl [path to zcl.json in SDK] --generationTemplate [path to generation templates.json in SDK] --in [path to input zap file] --inE [path to zap extension file] --noLoadingFailure --appendGenerationSubdirectory
18+
19+
### Launching UI
20+
21+
[ZAP executable] --packageMatch fuzzy --zcl [path to zcl.json in SDK] --generationTemplate [path to generation templates.json in SDK] --in [path to input zap file] --inE [path to zap extension file] --noLoadingFailure --appendGenerationSubdirectory
22+
23+
### References
24+
25+
- ZAP executable -> [ZAP binary](https://github.com/project-chip/zap/releases) or `node src-script/zap-start.js`

src-electron/importexport/import.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,68 @@ async function executePostImportScript(db, sessionId, scriptFile) {
8181
)
8282
}
8383

84+
/**
85+
* Merge extension file content into the base zap file content.
86+
*
87+
* @param {Object} baseState - The state object of the base .zap file.
88+
* @param {Object} extensionState - The state object of the extension .zap file.
89+
*/
90+
function mergeZapExtension(baseState, extensionState) {
91+
if (extensionState.endpoints) {
92+
extensionState.endpoints.forEach((extEndpoint) => {
93+
// Finding an endpoint using endpoint identifier linked to extension endpoint Id
94+
const baseEndpoint = baseState.endpoints.find(
95+
(et) => et.endpointId === extEndpoint.id
96+
)
97+
if (!baseEndpoint) {
98+
return
99+
}
100+
101+
// Finding the appropriate endpointType in the .zap file/base State
102+
const baseEndpointType =
103+
baseState.endpointTypes[baseEndpoint.endpointTypeIndex]
104+
105+
if (baseEndpointType) {
106+
extEndpoint.clusters.forEach((extCluster) => {
107+
const baseCluster = baseEndpointType.clusters.find(
108+
(bc) => bc.code === extCluster.code
109+
)
110+
111+
if (baseCluster) {
112+
// Merge attributes
113+
if (extCluster.attributes) {
114+
baseCluster.attributes = baseCluster.attributes || []
115+
extCluster.attributes.forEach((extAttr) => {
116+
const existingAttr = baseCluster.attributes.find(
117+
(attr) => attr.code === extAttr.code
118+
)
119+
if (!existingAttr) {
120+
baseCluster.attributes.push(extAttr)
121+
}
122+
})
123+
}
124+
// Merge commands
125+
if (extCluster.commands) {
126+
baseCluster.commands = baseCluster.commands || []
127+
extCluster.commands.forEach((extAttr) => {
128+
const existingCmd = baseCluster.commands.find(
129+
(attr) => attr.code === extAttr.code
130+
)
131+
if (!existingCmd) {
132+
baseCluster.commands.push(extAttr)
133+
}
134+
})
135+
}
136+
} else {
137+
// Add new cluster if it doesn't exist
138+
baseEndpointType.clusters.push(extCluster)
139+
}
140+
})
141+
}
142+
})
143+
}
144+
}
145+
84146
/**
85147
* Writes the data from the file into a new session.
86148
* NOTE: This function does NOT initialize session packages.
@@ -101,6 +163,20 @@ async function importDataFromFile(
101163
}
102164
) {
103165
let state = await readDataFromFile(filePath, options.defaultZclMetafile)
166+
// Merge extension files into the state
167+
if (
168+
options.extensionFiles &&
169+
Array.isArray(options.extensionFiles) &&
170+
options.extensionFiles.length > 0
171+
) {
172+
for (const extensionFile of options.extensionFiles) {
173+
const extensionState = await readDataFromFile(
174+
extensionFile,
175+
options.defaultZclMetafile
176+
)
177+
mergeZapExtension(state, extensionState)
178+
}
179+
}
104180

105181
state = ff.convertFromFile(state)
106182
try {

src-electron/main-process/startup.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,8 @@ async function generateSingleFile(
834834
packageMatch: options.packageMatch,
835835
defaultTemplateFile: options.template,
836836
upgradeZclPackages: upgradeZclPackages,
837-
upgradeTemplatePackages: upgradeTemplatePackages
837+
upgradeTemplatePackages: upgradeTemplatePackages,
838+
extensionFiles: options.extensionFiles
838839
})
839840
sessionId = importResult.sessionId
840841
output = outputFile(zapFile, outputPattern, index)
@@ -910,11 +911,13 @@ async function startGeneration(argv, options) {
910911
let zclProperties = argv.zclProperties
911912
let genResultFile = argv.genResultFile
912913
let skipPostGeneration = argv.skipPostGeneration
914+
let zapFileExtensions = argv.zapFileExtension ? [argv.zapFileExtension] : []
913915

914916
let hrstart = process.hrtime.bigint()
915917
options.logger(
916918
`🤖 ZAP generation started:
917919
🔍 input files: ${zapFiles}
920+
🔍 input Extension files: ${zapFileExtensions}
918921
🔍 output pattern: ${output}
919922
🔍 using templates: ${templateMetafile}
920923
🔍 using zcl data: ${zclProperties}
@@ -942,6 +945,10 @@ async function startGeneration(argv, options) {
942945
let globalTemplatePackageId = ctx.packageId
943946

944947
let files = gatherFiles(zapFiles, { suffix: '.zap', doBlank: true })
948+
let extensionFiles = gatherFiles(zapFileExtensions, {
949+
suffix: '.zapExtension',
950+
doBlank: false
951+
})
945952
if (files.length == 0) {
946953
options.logger(` 👎 no zap files found in: ${zapFiles}`)
947954
throw `👎 no zap files found in: ${zapFiles}`
@@ -959,6 +966,11 @@ async function startGeneration(argv, options) {
959966
// Used to upgrade the zap file during generation. Makes sure packages are
960967
// updated in .zap file during project creation in Studio.
961968
options.upgradeZapFile = argv.upgradeZapFile
969+
// Used for extending all the .zap files. Users can extend cluster
970+
// configurations on an endpoint type id mentioned
971+
if (extensionFiles && extensionFiles.length > 0) {
972+
options.extensionFiles = extensionFiles
973+
}
962974

963975
let nsDuration = process.hrtime.bigint() - hrstart
964976
options.logger(`🕐 Setup time: ${util.duration(nsDuration)} `)
@@ -1174,8 +1186,18 @@ async function startUpMainInstance(argv, callbacks) {
11741186
let uiEnabled = !argv.noUi
11751187
let zapFiles = argv.zapFiles
11761188
let port = await startNormal(quitFunction, argv)
1189+
let zapFileExtensions = null
1190+
if ('zapFileExtension' in argv) {
1191+
zapFileExtensions = [argv.zapFileExtension]
1192+
}
11771193
if (uiEnabled && uiFunction != null) {
1178-
uiFunction(port, zapFiles, argv.uiMode, argv.standalone)
1194+
uiFunction(
1195+
port,
1196+
zapFiles,
1197+
argv.uiMode,
1198+
argv.standalone,
1199+
zapFileExtensions
1200+
)
11791201
} else {
11801202
if (argv.showUrl) {
11811203
// NOTE: this is parsed/used by Studio as the default landing page.

src-electron/rest/file-ops.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ function httpPostFileOpen(db) {
4545
let search = req.body.search
4646
const query = new URLSearchParams(search)
4747
let file = query.get('filePath')
48+
// Gather .zapExtension files
49+
let zapFileExtensions = query.get('zapFileExtensions')
4850
if (file) {
4951
zapFilePath = file
5052
ideProjectPath = query.get('studioProject')
@@ -76,10 +78,15 @@ function httpPostFileOpen(db) {
7678
dbEnum.sessionKey.ideProjectPath,
7779
ideProjectPath
7880
)
79-
80-
let importResult = await importJs.importDataFromFile(db, zapFilePath, {
81-
sessionId: req.zapSessionId
82-
})
81+
let options = { sessionId: req.zapSessionId }
82+
if (zapFileExtensions) {
83+
options.extensionFiles = [zapFileExtensions]
84+
}
85+
let importResult = await importJs.importDataFromFile(
86+
db,
87+
zapFilePath,
88+
options
89+
)
8390

8491
let response = {
8592
sessionId: importResult.sessionId,

src-electron/rest/initialize.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function sessionAttempt(db) {
4040

4141
const query = new URLSearchParams(search)
4242
let filePath = query.get('filePath')
43+
let filePathExtension = query.get('zapFileExtensions')
4344
if (filePath) {
4445
if (filePath.includes('.zap')) {
4546
let data = await fsp.readFile(filePath)
@@ -72,7 +73,8 @@ function sessionAttempt(db) {
7273
sessions,
7374
filePath,
7475
zapFilePackages,
75-
open
76+
open,
77+
filePathExtension
7678
})
7779
} else {
7880
let open = true

src-electron/ui/ui-util.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,18 @@ function showErrorMessage(title, err) {
4949
* @param {*} db
5050
* @param {*} filePath
5151
* @param {*} httpPort Server port for the URL that will be constructed.
52+
* @param {*} zapFileExtensions Extend a zap file with zapExtension
5253
*/
53-
function openFileConfiguration(filePath, httpPort, standalone = false) {
54+
function openFileConfiguration(
55+
filePath,
56+
httpPort,
57+
standalone = false,
58+
zapFileExtensions = null
59+
) {
5460
window.windowCreate(httpPort, {
5561
filePath,
56-
standalone
62+
standalone,
63+
zapFileExtensions
5764
})
5865
}
5966

@@ -145,9 +152,10 @@ function openFileDialogAndReportResult(browserWindow, options) {
145152
* @param {*} zapFiles
146153
* @param {*} uiMode
147154
* @param {*} standalone
155+
* @param {*} zapFileExtensions
148156
* @returns promise of a file open configuration
149157
*/
150-
function enableUi(port, zapFiles, uiMode, standalone) {
158+
function enableUi(port, zapFiles, uiMode, standalone, zapFileExtensions) {
151159
window.initializeElectronUi(port)
152160
if (zapFiles.length == 0) {
153161
return openNewConfiguration(port, {
@@ -157,7 +165,7 @@ function enableUi(port, zapFiles, uiMode, standalone) {
157165
})
158166
} else {
159167
return util.executePromisesSequentially(zapFiles, (f) =>
160-
openFileConfiguration(f, port)
168+
openFileConfiguration(f, port, standalone, zapFileExtensions)
161169
)
162170
}
163171
}

src-electron/ui/window.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,14 @@ export function windowCreateIfNotThere(port) {
6767
* @param {*} restPort
6868
* @returns String
6969
*/
70-
function createQueryString(uiMode, standalone, isNew, filePath, restPort) {
70+
function createQueryString(
71+
uiMode,
72+
standalone,
73+
isNew,
74+
filePath,
75+
restPort,
76+
zapFileExtensions
77+
) {
7178
const params = new Map()
7279

7380
if (!arguments.length) {
@@ -88,6 +95,9 @@ function createQueryString(uiMode, standalone, isNew, filePath, restPort) {
8895
if (filePath !== undefined) {
8996
params.set('filePath', filePath)
9097
}
98+
if (Array.isArray(zapFileExtensions) && zapFileExtensions.length > 0) {
99+
params.set('zapFileExtensions', zapFileExtensions)
100+
}
91101

92102
// Electron/Development mode
93103
if (
@@ -145,7 +155,8 @@ export function windowCreate(port, args) {
145155
args?.standalone,
146156
args?.new,
147157
args?.filePath,
148-
httpServer.httpServerPort()
158+
httpServer.httpServerPort(),
159+
args?.zapFileExtensions
149160
)
150161

151162
// @ts-ignore

src-electron/util/args.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ export function processCommandLineArguments(argv) {
8686
type: 'string',
8787
default: null
8888
})
89+
.option('zapExtensionFile', {
90+
desc: 'input .zapExtension file to read in to extend the given .zap file.',
91+
alias: ['zapExtension', 'inE', 'iE', 'zapFileExtension'],
92+
type: 'string',
93+
default: null
94+
})
8995
.option('zclProperties', {
9096
desc: 'zcl.properties file to read in.',
9197
alias: ['zcl', 'z'],
@@ -271,11 +277,20 @@ For more information, see ${commonUrl.projectUrl}`
271277
if (typeof arg == 'number') return false
272278
if (arg.endsWith('.js')) return false
273279
if (commands.has(arg)) return false
280+
if (commands.has('.zapExtension')) return false
274281
return true
275282
})
283+
// Collect all zapExtensions
284+
let allZapFileExtensions = ret._.filter((arg) => {
285+
return arg.endsWith('.zapExtension')
286+
})
276287
if (ret.zapFile != null) allFiles.push(ret.zapFile)
277288
ret.zapFiles = allFiles
278289

290+
if (allZapFileExtensions && allZapFileExtensions.length > 0) {
291+
ret.zapFileExtensions = allZapFileExtensions
292+
}
293+
279294
if (ret.tempState) {
280295
let tempDir = fs.mkdtempSync(`${os.tmpdir()}${path.sep}zap.`)
281296
console.log(

0 commit comments

Comments
 (0)