@willieee802/zigbee-herdsman 0.49.3 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/.github/dependabot.yml +0 -3
  2. package/.github/workflows/ci.yml +1 -2
  3. package/.github/workflows/release-please.yml +1 -1
  4. package/.github/workflows/typedoc.yaml +3 -3
  5. package/.release-please-manifest.json +1 -1
  6. package/CHANGELOG.md +143 -0
  7. package/biome.json +1 -1
  8. package/dist/adapter/adapter.d.ts +14 -1
  9. package/dist/adapter/adapter.d.ts.map +1 -1
  10. package/dist/adapter/adapter.js +17 -0
  11. package/dist/adapter/adapter.js.map +1 -1
  12. package/dist/adapter/adapterDiscovery.d.ts.map +1 -1
  13. package/dist/adapter/adapterDiscovery.js.map +1 -1
  14. package/dist/adapter/deconz/adapter/deconzAdapter.d.ts +1 -3
  15. package/dist/adapter/deconz/adapter/deconzAdapter.d.ts.map +1 -1
  16. package/dist/adapter/deconz/adapter/deconzAdapter.js +14 -29
  17. package/dist/adapter/deconz/adapter/deconzAdapter.js.map +1 -1
  18. package/dist/adapter/deconz/driver/constants.d.ts +1 -1
  19. package/dist/adapter/deconz/driver/constants.d.ts.map +1 -1
  20. package/dist/adapter/ember/adapter/emberAdapter.d.ts +1 -1
  21. package/dist/adapter/ember/adapter/emberAdapter.d.ts.map +1 -1
  22. package/dist/adapter/ember/adapter/emberAdapter.js +19 -10
  23. package/dist/adapter/ember/adapter/emberAdapter.js.map +1 -1
  24. package/dist/adapter/ember/adapter/oneWaitress.d.ts +2 -0
  25. package/dist/adapter/ember/adapter/oneWaitress.d.ts.map +1 -1
  26. package/dist/adapter/ember/adapter/oneWaitress.js +13 -5
  27. package/dist/adapter/ember/adapter/oneWaitress.js.map +1 -1
  28. package/dist/adapter/ezsp/adapter/ezspAdapter.d.ts +1 -3
  29. package/dist/adapter/ezsp/adapter/ezspAdapter.d.ts.map +1 -1
  30. package/dist/adapter/ezsp/adapter/ezspAdapter.js +17 -30
  31. package/dist/adapter/ezsp/adapter/ezspAdapter.js.map +1 -1
  32. package/dist/adapter/ezsp/driver/index.d.ts +1 -1
  33. package/dist/adapter/ezsp/driver/index.d.ts.map +1 -1
  34. package/dist/adapter/ezsp/driver/index.js +1 -1
  35. package/dist/adapter/ezsp/driver/index.js.map +1 -1
  36. package/dist/adapter/ezsp/driver/types/index.d.ts +1 -1
  37. package/dist/adapter/ezsp/driver/types/index.d.ts.map +1 -1
  38. package/dist/adapter/ezsp/driver/types/index.js +3 -3
  39. package/dist/adapter/ezsp/driver/types/index.js.map +1 -1
  40. package/dist/adapter/serialPort.d.ts.map +1 -1
  41. package/dist/adapter/serialPort.js +7 -0
  42. package/dist/adapter/serialPort.js.map +1 -1
  43. package/dist/adapter/z-stack/adapter/adapter-backup.js +1 -1
  44. package/dist/adapter/z-stack/adapter/adapter-backup.js.map +1 -1
  45. package/dist/adapter/z-stack/adapter/adapter-nv-memory.js +1 -1
  46. package/dist/adapter/z-stack/adapter/adapter-nv-memory.js.map +1 -1
  47. package/dist/adapter/z-stack/adapter/manager.d.ts.map +1 -1
  48. package/dist/adapter/z-stack/adapter/manager.js +12 -2
  49. package/dist/adapter/z-stack/adapter/manager.js.map +1 -1
  50. package/dist/adapter/z-stack/adapter/tstype.d.ts.map +1 -1
  51. package/dist/adapter/z-stack/adapter/zStackAdapter.d.ts +1 -3
  52. package/dist/adapter/z-stack/adapter/zStackAdapter.d.ts.map +1 -1
  53. package/dist/adapter/z-stack/adapter/zStackAdapter.js +20 -34
  54. package/dist/adapter/z-stack/adapter/zStackAdapter.js.map +1 -1
  55. package/dist/adapter/z-stack/constants/index.d.ts +1 -1
  56. package/dist/adapter/z-stack/constants/index.d.ts.map +1 -1
  57. package/dist/adapter/z-stack/constants/index.js +1 -1
  58. package/dist/adapter/z-stack/constants/index.js.map +1 -1
  59. package/dist/adapter/z-stack/unpi/constants.d.ts +1 -1
  60. package/dist/adapter/z-stack/unpi/constants.d.ts.map +1 -1
  61. package/dist/adapter/z-stack/unpi/constants.js +1 -1
  62. package/dist/adapter/z-stack/unpi/constants.js.map +1 -1
  63. package/dist/adapter/zboss/adapter/zbossAdapter.d.ts +7 -8
  64. package/dist/adapter/zboss/adapter/zbossAdapter.d.ts.map +1 -1
  65. package/dist/adapter/zboss/adapter/zbossAdapter.js +12 -30
  66. package/dist/adapter/zboss/adapter/zbossAdapter.js.map +1 -1
  67. package/dist/adapter/zboss/driver.d.ts.map +1 -1
  68. package/dist/adapter/zboss/driver.js +8 -1
  69. package/dist/adapter/zboss/driver.js.map +1 -1
  70. package/dist/adapter/zboss/uart.d.ts.map +1 -1
  71. package/dist/adapter/zboss/uart.js +14 -2
  72. package/dist/adapter/zboss/uart.js.map +1 -1
  73. package/dist/adapter/zigate/adapter/zigateAdapter.d.ts +1 -3
  74. package/dist/adapter/zigate/adapter/zigateAdapter.d.ts.map +1 -1
  75. package/dist/adapter/zigate/adapter/zigateAdapter.js +8 -29
  76. package/dist/adapter/zigate/adapter/zigateAdapter.js.map +1 -1
  77. package/dist/adapter/zoh/adapter/zohAdapter.d.ts +1 -3
  78. package/dist/adapter/zoh/adapter/zohAdapter.d.ts.map +1 -1
  79. package/dist/adapter/zoh/adapter/zohAdapter.js +18 -33
  80. package/dist/adapter/zoh/adapter/zohAdapter.js.map +1 -1
  81. package/dist/controller/controller.d.ts.map +1 -1
  82. package/dist/controller/controller.js +10 -2
  83. package/dist/controller/controller.js.map +1 -1
  84. package/dist/controller/greenPower.d.ts.map +1 -1
  85. package/dist/controller/greenPower.js +15 -9
  86. package/dist/controller/greenPower.js.map +1 -1
  87. package/dist/controller/helpers/ota.d.ts +4 -4
  88. package/dist/controller/helpers/ota.d.ts.map +1 -1
  89. package/dist/controller/helpers/ota.js +28 -9
  90. package/dist/controller/helpers/ota.js.map +1 -1
  91. package/dist/controller/helpers/zclFrameConverter.d.ts.map +1 -1
  92. package/dist/controller/helpers/zclFrameConverter.js +17 -16
  93. package/dist/controller/helpers/zclFrameConverter.js.map +1 -1
  94. package/dist/controller/model/device.d.ts +14 -4
  95. package/dist/controller/model/device.d.ts.map +1 -1
  96. package/dist/controller/model/device.js +167 -85
  97. package/dist/controller/model/device.js.map +1 -1
  98. package/dist/controller/model/endpoint.d.ts +7 -3
  99. package/dist/controller/model/endpoint.d.ts.map +1 -1
  100. package/dist/controller/model/endpoint.js +34 -21
  101. package/dist/controller/model/endpoint.js.map +1 -1
  102. package/dist/controller/model/group.d.ts +0 -1
  103. package/dist/controller/model/group.d.ts.map +1 -1
  104. package/dist/controller/model/group.js +14 -19
  105. package/dist/controller/model/group.js.map +1 -1
  106. package/dist/controller/touchlink.js +3 -3
  107. package/dist/controller/touchlink.js.map +1 -1
  108. package/dist/utils/timeService.js +2 -2
  109. package/dist/utils/timeService.js.map +1 -1
  110. package/dist/zspec/zcl/buffaloZcl.d.ts +3 -3
  111. package/dist/zspec/zcl/buffaloZcl.d.ts.map +1 -1
  112. package/dist/zspec/zcl/buffaloZcl.js +198 -96
  113. package/dist/zspec/zcl/buffaloZcl.js.map +1 -1
  114. package/dist/zspec/zcl/definition/cluster.d.ts +2 -2
  115. package/dist/zspec/zcl/definition/cluster.d.ts.map +1 -1
  116. package/dist/zspec/zcl/definition/cluster.js +2699 -2808
  117. package/dist/zspec/zcl/definition/cluster.js.map +1 -1
  118. package/dist/zspec/zcl/definition/clusters-types.d.ts +63 -1109
  119. package/dist/zspec/zcl/definition/clusters-types.d.ts.map +1 -1
  120. package/dist/zspec/zcl/definition/enums.d.ts +0 -1
  121. package/dist/zspec/zcl/definition/enums.d.ts.map +1 -1
  122. package/dist/zspec/zcl/definition/enums.js +0 -1
  123. package/dist/zspec/zcl/definition/enums.js.map +1 -1
  124. package/dist/zspec/zcl/definition/foundation.d.ts +306 -7
  125. package/dist/zspec/zcl/definition/foundation.d.ts.map +1 -1
  126. package/dist/zspec/zcl/definition/foundation.js +552 -207
  127. package/dist/zspec/zcl/definition/foundation.js.map +1 -1
  128. package/dist/zspec/zcl/definition/status.d.ts +21 -10
  129. package/dist/zspec/zcl/definition/status.d.ts.map +1 -1
  130. package/dist/zspec/zcl/definition/status.js +11 -0
  131. package/dist/zspec/zcl/definition/status.js.map +1 -1
  132. package/dist/zspec/zcl/definition/tstype.d.ts +57 -48
  133. package/dist/zspec/zcl/definition/tstype.d.ts.map +1 -1
  134. package/dist/zspec/zcl/utils.d.ts +7 -4
  135. package/dist/zspec/zcl/utils.d.ts.map +1 -1
  136. package/dist/zspec/zcl/utils.js +133 -240
  137. package/dist/zspec/zcl/utils.js.map +1 -1
  138. package/dist/zspec/zcl/zclFrame.d.ts +4 -4
  139. package/dist/zspec/zcl/zclFrame.d.ts.map +1 -1
  140. package/dist/zspec/zcl/zclFrame.js +19 -103
  141. package/dist/zspec/zcl/zclFrame.js.map +1 -1
  142. package/dist/zspec/zcl/zclStatusError.d.ts +1 -1
  143. package/dist/zspec/zcl/zclStatusError.d.ts.map +1 -1
  144. package/dist/zspec/zcl/zclStatusError.js +2 -2
  145. package/dist/zspec/zcl/zclStatusError.js.map +1 -1
  146. package/package.json +1 -1
  147. package/scripts/clusters-typegen.ts +44 -139
  148. package/src/adapter/adapter.ts +38 -3
  149. package/src/adapter/adapterDiscovery.ts +2 -1
  150. package/src/adapter/deconz/adapter/deconzAdapter.ts +24 -51
  151. package/src/adapter/deconz/driver/constants.ts +1 -1
  152. package/src/adapter/ember/adapter/emberAdapter.ts +23 -10
  153. package/src/adapter/ember/adapter/oneWaitress.ts +16 -6
  154. package/src/adapter/ezsp/adapter/ezspAdapter.ts +27 -48
  155. package/src/adapter/ezsp/driver/index.ts +1 -1
  156. package/src/adapter/ezsp/driver/types/index.ts +99 -99
  157. package/src/adapter/serialPort.ts +9 -0
  158. package/src/adapter/z-stack/adapter/adapter-backup.ts +1 -1
  159. package/src/adapter/z-stack/adapter/adapter-nv-memory.ts +1 -1
  160. package/src/adapter/z-stack/adapter/manager.ts +16 -2
  161. package/src/adapter/z-stack/adapter/tstype.ts +1 -0
  162. package/src/adapter/z-stack/adapter/zStackAdapter.ts +34 -81
  163. package/src/adapter/z-stack/constants/index.ts +1 -1
  164. package/src/adapter/z-stack/unpi/constants.ts +1 -1
  165. package/src/adapter/zboss/adapter/zbossAdapter.ts +23 -54
  166. package/src/adapter/zboss/driver.ts +8 -1
  167. package/src/adapter/zboss/uart.ts +14 -1
  168. package/src/adapter/zigate/adapter/zigateAdapter.ts +17 -48
  169. package/src/adapter/zoh/adapter/zohAdapter.ts +27 -50
  170. package/src/controller/controller.ts +12 -2
  171. package/src/controller/greenPower.ts +16 -9
  172. package/src/controller/helpers/ota.ts +37 -11
  173. package/src/controller/helpers/zclFrameConverter.ts +20 -17
  174. package/src/controller/model/device.ts +204 -97
  175. package/src/controller/model/endpoint.ts +36 -24
  176. package/src/controller/model/group.ts +14 -20
  177. package/src/controller/touchlink.ts +3 -3
  178. package/src/utils/timeService.ts +2 -2
  179. package/src/zspec/zcl/buffaloZcl.ts +226 -100
  180. package/src/zspec/zcl/definition/cluster.ts +2713 -2822
  181. package/src/zspec/zcl/definition/clusters-types.ts +80 -1135
  182. package/src/zspec/zcl/definition/enums.ts +0 -1
  183. package/src/zspec/zcl/definition/foundation.ts +703 -216
  184. package/src/zspec/zcl/definition/status.ts +22 -11
  185. package/src/zspec/zcl/definition/tstype.ts +59 -58
  186. package/src/zspec/zcl/utils.ts +137 -264
  187. package/src/zspec/zcl/zclFrame.ts +25 -130
  188. package/src/zspec/zcl/zclStatusError.ts +2 -2
  189. package/test/adapter/ember/emberAdapter.test.ts +191 -4
  190. package/test/adapter/ezsp/uart.test.ts +10 -10
  191. package/test/adapter/z-stack/adapter.test.ts +88 -32
  192. package/test/adapter/zoh/zohAdapter.test.ts +4 -4
  193. package/test/controller.test.ts +822 -248
  194. package/test/device-ota.test.ts +141 -16
  195. package/test/device.test.ts +731 -0
  196. package/test/requests.bench.ts +2 -0
  197. package/test/zcl.test.ts +70 -95
  198. package/test/zspec/zcl/buffalo.test.ts +251 -11
  199. package/test/zspec/zcl/foundation.test.ts +990 -0
  200. package/test/zspec/zcl/frame.test.ts +84 -69
  201. package/test/zspec/zcl/utils.test.ts +105 -81
  202. package/tsconfig.json +0 -1
  203. package/scripts/check-clusters-changes.ts +0 -328
  204. package/scripts/clusters-changes.log +0 -584
  205. package/scripts/utils.ts +0 -88
  206. package/scripts/zap-update-clusters-report.json +0 -303
  207. package/scripts/zap-update-clusters.ts +0 -1520
  208. package/scripts/zap-update-types.ts +0 -707
  209. package/scripts/zap-xml-clusters-overrides-data.ts +0 -52
  210. package/scripts/zap-xml-clusters-overrides.ts +0 -400
  211. package/scripts/zap-xml-types.ts +0 -146
@@ -1,1520 +0,0 @@
1
- /**
2
- * Script: Update cluster.ts with data from ZCL XML files.
3
- *
4
- * Goals:
5
- * - Parse XML cluster definition files (Basic.xml, Time.xml, ElectricalMeasurement.xml, etc.)
6
- * - Respect XSD-described structure (cluster.xsd, type.xsd)
7
- * - Map <type:type> definitions (with inheritsFrom chains) to base ZCL primitive types
8
- * - Merge new attributes/commands into existing cluster definitions in src/zspec/zcl/definition/cluster.ts
9
- * - Do NOT overwrite existing attributes (comparison by numeric ID)
10
- * - Do NOT overwrite existing command parameters at existing indices (only append missing tail params)
11
- * - Add missing commands (decide placement: commands vs commandsResponse)
12
- * - Preserve existing formatting of cluster.ts as much as possible
13
- *
14
- * Requirements:
15
- * pnpm i xml2js @types/xml2js
16
- *
17
- * Usage:
18
- * tsx scripts/zap-update-clusters.ts ../zap/zcl-builtin/dotdot
19
- */
20
-
21
- import {promises as fs} from "node:fs";
22
- import path from "node:path";
23
- import process from "node:process";
24
- import ts from "typescript";
25
- import {parseStringPromise} from "xml2js";
26
- // biome-ignore lint/correctness/noUnusedImports: reference names (not executed in script context, but helps validation if compiled in-project)
27
- import {BuffaloZclDataType, DataType} from "../src/zspec/zcl/definition/enums.js";
28
- import {fuzzyMatch} from "./utils.js";
29
- import {applyXmlOverrides, parseOverrides} from "./zap-xml-clusters-overrides.js";
30
- import type {
31
- XMLAttributeDefinition,
32
- XMLCluster,
33
- XMLClusterSide,
34
- XMLCommandDefinition,
35
- XMLFieldDefinition,
36
- XMLRoot,
37
- XMLTypeType,
38
- } from "./zap-xml-types.js";
39
-
40
- // #region Type Definitions
41
-
42
- interface ParsedXMLClusterData {
43
- id: number;
44
- name: string;
45
- attributes: ParsedXMLAttribute[];
46
- serverCommands: ParsedXMLCommand[];
47
- clientCommands: ParsedXMLCommand[];
48
- }
49
-
50
- interface ParsedXMLAttribute {
51
- id: number;
52
- name: string;
53
- dataTypeExpr: string;
54
- meta?: XMLAttributeDefinition;
55
- client?: boolean;
56
- }
57
-
58
- interface ParsedXMLCommand {
59
- id: number;
60
- name: string;
61
- isResponse: boolean;
62
- parameters: ParsedXMLCommandParameter[];
63
- meta?: XMLCommandDefinition;
64
- client?: boolean;
65
- }
66
-
67
- interface ParsedXMLCommandParameter {
68
- name: string;
69
- dataTypeExpr: string;
70
- meta?: XMLFieldDefinition;
71
- isLength?: boolean;
72
- }
73
-
74
- interface ValidationRecord {
75
- warnings: string[];
76
- errors: string[];
77
- unknownTypes: Set<string>;
78
- addedAttributes: string[];
79
- addedCommands: string[];
80
- addedParameters: string[];
81
- scannedClusters: string[];
82
- changedClusters: string[];
83
- xmlClustersMissingFromTs: Map<string, string>;
84
- tsClustersMissingFromXml: string[];
85
- }
86
-
87
- // #endregion
88
-
89
- // #region Helpers
90
-
91
- function parseAttributeOrCommandId(hexId: string): number {
92
- const clean = hexId.trim().toLowerCase();
93
-
94
- return Number.parseInt(clean, 16);
95
- }
96
-
97
- function normalizeAttributeOrCommandName(name: string): string {
98
- if (!name) {
99
- return name;
100
- }
101
-
102
- const first = name[0].toLowerCase();
103
-
104
- return `${first}${name.slice(1)}`.replace(/[^A-Za-z0-9]/g, "");
105
- }
106
-
107
- function findBestFuzzyAttrMatch(
108
- refName: string,
109
- candidates: ParsedXMLAttribute[],
110
- existingTsNames: Map<number, string>,
111
- factory: ts.NodeFactory,
112
- ): ts.StringLiteral | undefined {
113
- if (candidates.length === 0) {
114
- return undefined;
115
- }
116
-
117
- let bestMatch: {score: number; attribute: ParsedXMLAttribute | undefined} = {score: -1, attribute: undefined};
118
- const normalizedRefName = normalizeAttributeOrCommandName(refName);
119
-
120
- for (const candidate of candidates) {
121
- const score = fuzzyMatch(normalizedRefName, candidate.name);
122
-
123
- if (score > bestMatch.score) {
124
- bestMatch = {score, attribute: candidate};
125
- }
126
- }
127
-
128
- if (bestMatch.attribute && bestMatch.score > 0.7) {
129
- // If a match is found, check if it has a corresponding name in the existing TS file.
130
- // If so, use that name. Otherwise, fall back to the normalized XML name.
131
- const tsName = existingTsNames.get(bestMatch.attribute.id);
132
- const finalName = tsName || bestMatch.attribute.name;
133
-
134
- return factory.createStringLiteral(finalName);
135
- }
136
-
137
- return undefined;
138
- }
139
-
140
- function findBestFuzzyParamMatch(
141
- refName: string,
142
- candidates: ParsedXMLCommandParameter[],
143
- existingTsParamNames: Map<string, string>,
144
- factory: ts.NodeFactory,
145
- ): ts.StringLiteral | undefined {
146
- if (candidates.length === 0) {
147
- return undefined;
148
- }
149
-
150
- let bestMatch: {score: number; parameter: ParsedXMLCommandParameter | undefined} = {score: -1, parameter: undefined};
151
- const normalizedRefName = normalizeAttributeOrCommandName(refName);
152
-
153
- for (const candidate of candidates) {
154
- const score = fuzzyMatch(normalizedRefName, candidate.name);
155
-
156
- if (score > bestMatch.score) {
157
- bestMatch = {score, parameter: candidate};
158
- }
159
- }
160
-
161
- if (bestMatch.parameter && bestMatch.score > 0.7) {
162
- // If a match is found, check if it has a corresponding name in the existing TS file.
163
- // If so, use that name. Otherwise, fall back to the normalized XML name.
164
- const tsName = existingTsParamNames.get(bestMatch.parameter.name);
165
- const finalName = tsName || bestMatch.parameter.name;
166
-
167
- return factory.createStringLiteral(finalName);
168
- }
169
-
170
- return undefined;
171
- }
172
-
173
- // #endregion
174
-
175
- // #region XML processing
176
-
177
- async function collectFromLibrary(libraryFile: string): Promise<{includeFiles: string[]; libraryTypes: XMLTypeType[]}> {
178
- const txt = await fs.readFile(libraryFile, "utf8");
179
-
180
- const parsed = (await parseStringPromise(txt, {
181
- explicitArray: true,
182
- preserveChildrenOrder: true,
183
- mergeAttrs: false,
184
- explicitRoot: true,
185
- })) as XMLRoot;
186
-
187
- const includeFiles: string[] = [];
188
- const libraryTypes: XMLTypeType[] = [];
189
- const libs = parsed["zcl:library"];
190
-
191
- if (libs) {
192
- const libsArray = Array.isArray(libs) ? libs : [libs];
193
-
194
- for (const lib of libsArray) {
195
- if (lib["type:type"]) {
196
- for (const t of lib["type:type"]) {
197
- libraryTypes.push(t);
198
- }
199
- }
200
-
201
- if (lib["xi:include"]) {
202
- const baseDir = path.dirname(libraryFile);
203
-
204
- for (const inc of lib["xi:include"]) {
205
- const parse = inc.$.parse;
206
- const href = inc.$.href;
207
-
208
- if (parse === "xml" && href.toLowerCase().endsWith(".xml")) {
209
- const full = path.resolve(baseDir, href);
210
- includeFiles.push(full);
211
- }
212
- }
213
- }
214
- }
215
- }
216
-
217
- return {includeFiles, libraryTypes};
218
- }
219
-
220
- async function parseXMLClusters(file: string): Promise<XMLCluster[]> {
221
- const text = await fs.readFile(file, "utf8");
222
-
223
- const parsed = (await parseStringPromise(text, {
224
- explicitArray: true,
225
- preserveChildrenOrder: true,
226
- mergeAttrs: false,
227
- explicitRoot: true,
228
- })) as XMLRoot;
229
-
230
- const clusters: XMLCluster[] = [];
231
- const baseClusters = parsed["zcl:cluster"];
232
- // const derivedClusters = parsed["zcl:derivedCluster"];
233
-
234
- if (baseClusters) {
235
- if (Array.isArray(baseClusters)) {
236
- clusters.push(...baseClusters);
237
- } else {
238
- clusters.push(baseClusters);
239
- }
240
- }
241
-
242
- // if (derivedClusters) {
243
- // if (Array.isArray(derivedClusters)) {
244
- // clusters.push(...derivedClusters);
245
- // } else {
246
- // clusters.push(derivedClusters);
247
- // }
248
- // }
249
-
250
- return clusters;
251
- }
252
-
253
- function processClusterSide(
254
- side: XMLClusterSide | undefined,
255
- isClient: boolean,
256
- resolver: TypeResolver,
257
- validation: ValidationRecord,
258
- ): {attributes: ParsedXMLAttribute[]; commands: ParsedXMLCommand[]} {
259
- const attributes: ParsedXMLAttribute[] = [];
260
- const commands: ParsedXMLCommand[] = [];
261
-
262
- if (!side) {
263
- return {attributes, commands};
264
- }
265
-
266
- if (side.attributes && side.attributes.length > 0) {
267
- const attrs = side.attributes[0].attribute;
268
-
269
- if (attrs) {
270
- for (const a of attrs) {
271
- if (!a?.$?.id) {
272
- continue;
273
- }
274
-
275
- const attrId = parseAttributeOrCommandId(a.$.id);
276
- const attrTypeShortRaw = a.$.type.trim().toLowerCase();
277
- const baseShort = resolver.resolve(attrTypeShortRaw);
278
- const mappedDataType = mapZclTypeToDataType(baseShort);
279
-
280
- if (mappedDataType === "DataType.UNKNOWN" && attrTypeShortRaw !== "unk") {
281
- validation.unknownTypes.add(attrTypeShortRaw);
282
- }
283
-
284
- const attrName = normalizeAttributeOrCommandName(a.$.name);
285
- const attribute: ParsedXMLAttribute = {id: attrId, name: attrName, dataTypeExpr: mappedDataType, meta: a};
286
-
287
- if (isClient) {
288
- attribute.client = true;
289
- }
290
-
291
- attributes.push(attribute);
292
- }
293
- }
294
- }
295
-
296
- if (side.commands && side.commands.length > 0) {
297
- const cmds = side.commands[0].command;
298
-
299
- if (cmds) {
300
- for (const cmd of cmds) {
301
- if (!cmd?.$?.id) {
302
- continue;
303
- }
304
-
305
- const cmdId = parseAttributeOrCommandId(cmd.$.id);
306
- const cmdNameRaw = cmd.$.name;
307
- const cmdName = normalizeAttributeOrCommandName(cmdNameRaw);
308
- const parameters: ParsedXMLCommandParameter[] = [];
309
-
310
- if (cmd.fields && cmd.fields.length > 0) {
311
- const fields = cmd.fields[0].field;
312
-
313
- if (fields) {
314
- for (const [paramIndex, fld] of fields.entries()) {
315
- const paramName = normalizeAttributeOrCommandName(fld.$.name);
316
- const typeShortRaw = fld.$.type.trim().toLowerCase();
317
- const baseShort = resolver.resolve(typeShortRaw);
318
- let mapped = mapZclTypeToDataType(baseShort);
319
- const arrayFlag = fld.$.array === "true";
320
-
321
- if (mapped === "DataType.UNKNOWN" && typeShortRaw !== "unk") {
322
- validation.unknownTypes.add(typeShortRaw);
323
- }
324
-
325
- if (arrayFlag && !fld.$.arrayLengthSize && !fld.$.arrayLengthField) {
326
- // This field represents an array with an implicit count.
327
- // Create the count parameter.
328
- parameters.push({
329
- name: `${paramName}Count`,
330
- dataTypeExpr: "DataType.UINT8", // The implicit count is usually UINT8
331
- meta: fld, // Carry over meta for context, but it's for the count
332
- isLength: true,
333
- });
334
-
335
- // Now, create the actual array parameter.
336
- mapped = mapToBuffaloZclDataType(mapped);
337
-
338
- parameters.push({
339
- name: paramName,
340
- dataTypeExpr: mapped,
341
- meta: fld,
342
- });
343
- } else {
344
- // Original logic for non-arrays or arrays with explicit length fields.
345
- if (arrayFlag) {
346
- mapped = mapToBuffaloZclDataType(mapped);
347
- }
348
-
349
- parameters.push({
350
- name: paramName || `param${paramIndex}`,
351
- dataTypeExpr: mapped,
352
- meta: fld,
353
- });
354
- }
355
- }
356
- }
357
- }
358
-
359
- const command: ParsedXMLCommand = {id: cmdId, name: cmdName, isResponse: isClient, parameters, meta: cmd};
360
-
361
- if (isClient) {
362
- command.client = true;
363
- }
364
-
365
- commands.push(command);
366
- }
367
- }
368
- }
369
-
370
- return {attributes, commands};
371
- }
372
-
373
- function parseClustersFromXML(list: XMLCluster[], globalResolver: TypeResolver, validation: ValidationRecord): ParsedXMLClusterData[] {
374
- const out: ParsedXMLClusterData[] = [];
375
-
376
- for (const cluster of list) {
377
- const idHex = cluster.$.id;
378
-
379
- if (!idHex) {
380
- continue;
381
- }
382
-
383
- // Create a new resolver for this specific cluster, with the global resolver as its parent.
384
- const clusterResolver = new TypeResolver(globalResolver);
385
-
386
- // Add only this cluster's local types to it.
387
- clusterResolver.add(cluster["type:type"]);
388
-
389
- const idNum = parseAttributeOrCommandId(idHex);
390
- const idHexStr = `0x${idNum.toString(16).padStart(4, "0")}`;
391
- const name = cluster.$.name;
392
-
393
- validation.scannedClusters.push(`${idHexStr} ${name}`);
394
- validation.xmlClustersMissingFromTs.set(idHexStr, `${idHexStr} ${name}`);
395
-
396
- // Process the cluster using its own scoped resolver.
397
- const serverData = processClusterSide(cluster.server?.[0], false, clusterResolver, validation);
398
- const clientData = processClusterSide(cluster.client?.[0], true, clusterResolver, validation);
399
-
400
- out.push({
401
- id: idNum,
402
- name,
403
- attributes: [...serverData.attributes, ...clientData.attributes],
404
- serverCommands: serverData.commands,
405
- clientCommands: clientData.commands,
406
- });
407
- }
408
-
409
- return out;
410
- }
411
-
412
- // #endregion
413
-
414
- // #region Mapping types
415
-
416
- class TypeResolver {
417
- #map: Map<string, string | undefined /* inheritsFrom */>;
418
- #parent?: TypeResolver;
419
-
420
- constructor(parent?: TypeResolver) {
421
- this.#map = new Map();
422
- this.#parent = parent;
423
- }
424
-
425
- add(types: XMLTypeType[] | undefined): void {
426
- if (!types) {
427
- return;
428
- }
429
-
430
- for (const t of types) {
431
- const short = t.$.short.trim().toLowerCase();
432
- const inheritsFrom = t.$.inheritsFrom ? t.$.inheritsFrom.trim().toLowerCase() : undefined;
433
-
434
- if (!this.#map.has(short)) {
435
- this.#map.set(short, inheritsFrom);
436
- }
437
- }
438
- }
439
-
440
- resolve(short: string): string {
441
- if (this.#map.has(short)) {
442
- return this.#map.get(short) ?? short;
443
- }
444
-
445
- return this.#parent?.resolve(short) ?? "unk";
446
- }
447
- }
448
-
449
- function mapZclTypeToDataType(base: string): string {
450
- const t = base.toLowerCase();
451
-
452
- if (t === "data8") return "DataType.DATA8";
453
- if (t === "data16") return "DataType.DATA16";
454
- if (t === "data24") return "DataType.DATA24";
455
- if (t === "data32") return "DataType.DATA32";
456
- if (t === "data40") return "DataType.DATA40";
457
- if (t === "data48") return "DataType.DATA48";
458
- if (t === "data56") return "DataType.DATA56";
459
- if (t === "data64") return "DataType.DATA64";
460
-
461
- if (t === "bool") return "DataType.BOOLEAN";
462
-
463
- if (t === "map8") return "DataType.BITMAP8";
464
- if (t === "map16") return "DataType.BITMAP16";
465
- if (t === "map24") return "DataType.BITMAP24";
466
- if (t === "map32") return "DataType.BITMAP32";
467
- if (t === "map40") return "DataType.BITMAP40";
468
- if (t === "map48") return "DataType.BITMAP48";
469
- if (t === "map56") return "DataType.BITMAP56";
470
- if (t === "map64") return "DataType.BITMAP64";
471
-
472
- if (t === "uint8") return "DataType.UINT8";
473
- if (t === "uint16") return "DataType.UINT16";
474
- if (t === "uint24") return "DataType.UINT24";
475
- if (t === "uint32") return "DataType.UINT32";
476
- if (t === "uint40") return "DataType.UINT40";
477
- if (t === "uint48") return "DataType.UINT48";
478
- if (t === "uint56") return "DataType.UINT56";
479
- if (t === "uint64") return "DataType.UINT64";
480
-
481
- if (t === "int8") return "DataType.INT8";
482
- if (t === "int16") return "DataType.INT16";
483
- if (t === "int24") return "DataType.INT24";
484
- if (t === "int32") return "DataType.INT32";
485
- if (t === "int40") return "DataType.INT40";
486
- if (t === "int48") return "DataType.INT48";
487
- if (t === "int56") return "DataType.INT56";
488
- if (t === "int64") return "DataType.INT64";
489
-
490
- if (t === "enum8") return "DataType.ENUM8";
491
- if (t === "enum16") return "DataType.ENUM16";
492
-
493
- if (t === "semi") return "DataType.SEMI_PREC";
494
- if (t === "single") return "DataType.SINGLE_PREC";
495
- if (t === "double") return "DataType.DOUBLE_PREC";
496
-
497
- if (t === "octstr") return "DataType.OCTET_STR";
498
- if (t === "string") return "DataType.CHAR_STR";
499
- if (t === "octstr16") return "DataType.LONG_OCTET_STR";
500
- if (t === "string16") return "DataType.LONG_CHAR_STR";
501
-
502
- if (t === "array") return "DataType.ARRAY";
503
- if (t === "struct") return "DataType.STRUCT";
504
-
505
- if (t === "set") return "DataType.SET";
506
- if (t === "bag") return "DataType.BAG";
507
-
508
- if (t === "tod") return "DataType.TOD";
509
- if (t === "date") return "DataType.DATE";
510
- if (t === "utc") return "DataType.UTC";
511
-
512
- if (t === "clusterid") return "DataType.CLUSTER_ID";
513
- if (t === "attribid") return "DataType.ATTR_ID";
514
- if (t === "bacoid") return "DataType.BAC_OID";
515
-
516
- if (t === "eui64") return "DataType.IEEE_ADDR";
517
- if (t === "key128") return "DataType.SEC_KEY";
518
-
519
- if (t === "unk") return "DataType.UNKNOWN";
520
- if (t === "zcltype") return "DataType.ZCLTYPE";
521
- if (t === "attributereportingstatus") return "DataType.ENUM8";
522
- if (t === "zclstatus") return "DataType.ENUM8";
523
- if (t === "profileintervalperiod") return "DataType.ENUM8";
524
- if (t === "iaszonetype") return "DataType.ENUM16";
525
- if (t === "iaszonestatus") return "DataType.BITMAP16";
526
-
527
- if (t === "sextensionfieldsetlist") return "BuffaloZclDataType.EXTENSION_FIELD_SETS";
528
- if (t === "transitiontype") return "BuffaloZclDataType.LIST_THERMO_TRANSITIONS";
529
- if (t === "iasacezonestatusrecord") return "BuffaloZclDataType.LIST_ZONEINFO";
530
-
531
- return "DataType.UNKNOWN";
532
- }
533
-
534
- function isNumericDataType(dataTypeExpr: string): boolean {
535
- switch (dataTypeExpr) {
536
- case "DataType.DATA8":
537
- case "DataType.DATA16":
538
- case "DataType.DATA24":
539
- case "DataType.DATA32":
540
- case "DataType.DATA40":
541
- case "DataType.DATA48":
542
- case "DataType.DATA56":
543
- case "DataType.DATA64":
544
- case "DataType.BOOLEAN":
545
- case "DataType.BITMAP8":
546
- case "DataType.BITMAP16":
547
- case "DataType.BITMAP24":
548
- case "DataType.BITMAP32":
549
- case "DataType.BITMAP40":
550
- case "DataType.BITMAP48":
551
- case "DataType.BITMAP56":
552
- case "DataType.BITMAP64":
553
- case "DataType.UINT8":
554
- case "DataType.UINT16":
555
- case "DataType.UINT24":
556
- case "DataType.UINT32":
557
- case "DataType.UINT40":
558
- case "DataType.UINT48":
559
- case "DataType.UINT56":
560
- case "DataType.UINT64":
561
- case "DataType.INT8":
562
- case "DataType.INT16":
563
- case "DataType.INT24":
564
- case "DataType.INT32":
565
- case "DataType.INT40":
566
- case "DataType.INT48":
567
- case "DataType.INT56":
568
- case "DataType.INT64":
569
- case "DataType.ENUM8":
570
- case "DataType.ENUM16":
571
- case "DataType.SEMI_PREC":
572
- case "DataType.SINGLE_PREC":
573
- case "DataType.DOUBLE_PREC":
574
- case "DataType.CLUSTER_ID":
575
- case "DataType.ATTR_ID":
576
- case "DataType.BAC_OID":
577
- case "DataType.UTC":
578
- return true;
579
- default:
580
- return false;
581
- }
582
- }
583
-
584
- function mapToBuffaloZclDataType(base: string): string {
585
- if (base === "DataType.UINT8") return "BuffaloZclDataType.LIST_UINT8";
586
- if (base === "DataType.UINT16") return "BuffaloZclDataType.LIST_UINT16";
587
- if (base === "DataType.UINT24") return "BuffaloZclDataType.LIST_UINT24";
588
- if (base === "DataType.UINT32") return "BuffaloZclDataType.LIST_UINT32";
589
- if (base.startsWith("DataType")) return "BuffaloZclDataType.BUFFER";
590
-
591
- return base;
592
- }
593
-
594
- // #endregion
595
-
596
- // #region AST Transformer
597
-
598
- const findProperty = (obj: ts.ObjectLiteralExpression, name: string): ts.PropertyAssignment | undefined => {
599
- for (const p of obj.properties) {
600
- if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === name) {
601
- return p;
602
- }
603
- }
604
-
605
- return undefined;
606
- };
607
-
608
- const extractNumericId = (node: ts.Node): number | undefined => {
609
- if (!ts.isPropertyAssignment(node)) {
610
- return undefined;
611
- }
612
-
613
- if (!ts.isObjectLiteralExpression(node.initializer)) {
614
- return undefined;
615
- }
616
-
617
- const idProp = findProperty(node.initializer, "ID");
618
-
619
- if (!idProp) {
620
- return undefined;
621
- }
622
-
623
- const initializer = idProp.initializer;
624
-
625
- if (ts.isNumericLiteral(initializer)) {
626
- const text = initializer.text;
627
-
628
- return text.startsWith("0x") ? Number.parseInt(text, 16) : Number(text);
629
- }
630
-
631
- if (ts.isPrefixUnaryExpression(initializer) && ts.isNumericLiteral(initializer.operand)) {
632
- const text = initializer.operand.text;
633
- const num = text.startsWith("0x") ? Number.parseInt(text, 16) : Number(text);
634
-
635
- return -num;
636
- }
637
-
638
- return undefined;
639
- };
640
-
641
- function createSafeNumericLiteral(value: string | number, factory: ts.NodeFactory): ts.Expression {
642
- const num = Number(value);
643
-
644
- if (Number.isNaN(num)) {
645
- // Fallback for invalid numbers, though this case should be rare.
646
- return factory.createNumericLiteral(0);
647
- }
648
-
649
- if (num < 0) {
650
- return factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(num)));
651
- }
652
-
653
- return factory.createNumericLiteral(num);
654
- }
655
-
656
- function getExistingTsAttributeNames(attributesProp: ts.PropertyAssignment | undefined): Map<number, string> {
657
- const existingTsNames = new Map<number, string>();
658
-
659
- if (!attributesProp || !ts.isObjectLiteralExpression(attributesProp.initializer)) {
660
- return existingTsNames;
661
- }
662
-
663
- for (const attr of attributesProp.initializer.properties) {
664
- if (ts.isPropertyAssignment(attr)) {
665
- const id = extractNumericId(attr);
666
-
667
- if (id !== undefined && ts.isIdentifier(attr.name) && !existingTsNames.has(id)) {
668
- existingTsNames.set(id, attr.name.text);
669
- }
670
- }
671
- }
672
-
673
- return existingTsNames;
674
- }
675
-
676
- function createUpdateTransformer(
677
- xmlClusterData: Map<number, ParsedXMLClusterData>,
678
- validation: ValidationRecord,
679
- ): ts.TransformerFactory<ts.SourceFile> {
680
- return (context) => {
681
- const factory = context.factory;
682
-
683
- const visitor: ts.Visitor = (node) => {
684
- if (
685
- ts.isVariableDeclaration(node) &&
686
- ts.isIdentifier(node.name) &&
687
- node.name.text === "Clusters" &&
688
- node.initializer &&
689
- ts.isObjectLiteralExpression(node.initializer)
690
- ) {
691
- const tsClusterIds = new Set<number>();
692
- const newClusterProps: ts.ObjectLiteralElementLike[] = [];
693
-
694
- for (const prop of node.initializer.properties) {
695
- if (!ts.isPropertyAssignment(prop) || !ts.isObjectLiteralExpression(prop.initializer)) {
696
- newClusterProps.push(prop);
697
-
698
- continue;
699
- }
700
-
701
- const clusterId = extractNumericId(prop);
702
-
703
- if (clusterId !== undefined) {
704
- tsClusterIds.add(clusterId);
705
- } else {
706
- newClusterProps.push(prop);
707
-
708
- continue;
709
- }
710
-
711
- const xmlCluster = xmlClusterData.get(clusterId);
712
-
713
- if (!xmlCluster) {
714
- newClusterProps.push(prop);
715
-
716
- continue;
717
- }
718
-
719
- const clusterIdHexStr = `0x${clusterId.toString(16).padStart(4, "0")}`;
720
-
721
- validation.xmlClustersMissingFromTs.delete(clusterIdHexStr); // Found it
722
-
723
- let changed = false;
724
- const newSubProps: ts.ObjectLiteralElementLike[] = [];
725
- const subPropsMap = new Map<string, ts.PropertyAssignment>();
726
-
727
- for (const subProp of prop.initializer.properties) {
728
- if (ts.isPropertyAssignment(subProp) && ts.isIdentifier(subProp.name)) {
729
- subPropsMap.set(subProp.name.text, subProp);
730
- }
731
- }
732
-
733
- const attributesProp = subPropsMap.get("attributes");
734
- const existingTsAttributeNames = getExistingTsAttributeNames(attributesProp);
735
-
736
- for (const subProp of prop.initializer.properties) {
737
- if (!ts.isPropertyAssignment(subProp) || !ts.isIdentifier(subProp.name)) {
738
- newSubProps.push(subProp);
739
-
740
- continue;
741
- }
742
-
743
- if (!ts.isObjectLiteralExpression(subProp.initializer)) {
744
- newSubProps.push(subProp);
745
-
746
- continue;
747
- }
748
-
749
- const subPropName = subProp.name.text;
750
- let updatedProp: ts.PropertyAssignment | undefined;
751
-
752
- if (subPropName === "attributes") {
753
- updatedProp = updateAttributes(subProp, xmlCluster.attributes, factory, validation);
754
- } else if (subPropName === "commands") {
755
- updatedProp = updateCommands(
756
- subProp,
757
- xmlCluster.serverCommands,
758
- xmlCluster.attributes,
759
- existingTsAttributeNames,
760
- false,
761
- factory,
762
- validation,
763
- );
764
- } else if (subPropName === "commandsResponse") {
765
- updatedProp = updateCommands(
766
- subProp,
767
- xmlCluster.clientCommands,
768
- xmlCluster.attributes,
769
- existingTsAttributeNames,
770
- true,
771
- factory,
772
- validation,
773
- );
774
- }
775
-
776
- if (updatedProp && updatedProp !== subProp) {
777
- changed = true;
778
-
779
- newSubProps.push(updatedProp);
780
- } else {
781
- newSubProps.push(subProp);
782
- }
783
- }
784
-
785
- if (changed) {
786
- validation.changedClusters.push(`${clusterIdHexStr} ${xmlCluster.name}`);
787
- const updatedInitializer = factory.updateObjectLiteralExpression(prop.initializer, newSubProps);
788
- const updatedProperty = factory.updatePropertyAssignment(prop, prop.name, updatedInitializer);
789
-
790
- newClusterProps.push(updatedProperty);
791
- } else {
792
- newClusterProps.push(prop);
793
- }
794
- }
795
-
796
- // Populate clusters present in TS but not in XML
797
- for (const id of tsClusterIds) {
798
- if (!xmlClusterData.has(id) && id <= 0x7fff /* std cluster */) {
799
- validation.tsClustersMissingFromXml.push(`0x${id.toString(16).padStart(4, "0")}`);
800
- }
801
- }
802
-
803
- const updatedInitializer = factory.updateObjectLiteralExpression(node.initializer, newClusterProps);
804
-
805
- return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, updatedInitializer);
806
- }
807
-
808
- return ts.visitEachChild(node, visitor, context);
809
- };
810
-
811
- return (sourceFile) => ts.visitNode(sourceFile, visitor) as ts.SourceFile;
812
- };
813
- }
814
-
815
- function createHexIdTransformer(): ts.TransformerFactory<ts.SourceFile> {
816
- return (context) => {
817
- const factory = context.factory;
818
- const visitor: ts.Visitor = (node) => {
819
- if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "ID" && ts.isNumericLiteral(node.initializer)) {
820
- const num = Number(node.initializer.text);
821
-
822
- if (!Number.isNaN(num) && !node.initializer.text.startsWith("0x")) {
823
- let pad = 4; // Default to 4 for clusters and attributes
824
- let current: ts.Node = node.parent;
825
-
826
- // Traverse up to find if the ID is inside a `commands` or `commandsResponse` block.
827
- while (current?.parent) {
828
- if (ts.isPropertyAssignment(current.parent) && ts.isIdentifier(current.parent.name)) {
829
- const parentName = current.parent.name.text;
830
-
831
- if (parentName === "commands" || parentName === "commandsResponse") {
832
- pad = 2;
833
- break;
834
- }
835
- }
836
-
837
- current = current.parent;
838
- }
839
-
840
- const hex = `0x${num.toString(16).padStart(pad, "0")}`;
841
-
842
- return factory.updatePropertyAssignment(node, node.name, factory.createNumericLiteral(hex));
843
- }
844
- }
845
-
846
- return ts.visitEachChild(node, visitor, context);
847
- };
848
-
849
- return (sourceFile) => ts.visitNode(sourceFile, visitor) as ts.SourceFile;
850
- };
851
- }
852
-
853
- function updateAttributes(
854
- attributesProp: ts.PropertyAssignment,
855
- xmlAttributes: ParsedXMLAttribute[],
856
- factory: ts.NodeFactory,
857
- validation: ValidationRecord,
858
- ): ts.PropertyAssignment {
859
- const initializer = attributesProp.initializer as ts.ObjectLiteralExpression;
860
- const existingTsAttributes = new Map<number, ts.PropertyAssignment[]>();
861
- const existingTsNames = new Map<number, string>();
862
-
863
- for (const attr of initializer.properties) {
864
- if (ts.isPropertyAssignment(attr)) {
865
- const id = extractNumericId(attr);
866
-
867
- if (id !== undefined) {
868
- if (!existingTsAttributes.has(id)) {
869
- existingTsAttributes.set(id, []);
870
- }
871
-
872
- // biome-ignore lint/style/noNonNullAssertion: set above if not exist
873
- existingTsAttributes.get(id)!.push(attr);
874
-
875
- if (ts.isIdentifier(attr.name) && !existingTsNames.has(id)) {
876
- existingTsNames.set(id, attr.name.text);
877
- }
878
- }
879
- }
880
- }
881
-
882
- const finalAttributes: ts.PropertyAssignment[] = [];
883
- const processedTsNodes = new Set<ts.PropertyAssignment>();
884
-
885
- for (const xmlAttr of xmlAttributes) {
886
- const existingNodesWithId = existingTsAttributes.get(xmlAttr.id) || [];
887
- let existingAttrNode: ts.PropertyAssignment | undefined;
888
-
889
- for (const node of existingNodesWithId) {
890
- const initializer = node.initializer as ts.ObjectLiteralExpression;
891
- const manuCodeProp = findProperty(initializer, "manufacturerCode");
892
-
893
- if (!manuCodeProp) {
894
- existingAttrNode = node;
895
-
896
- break;
897
- }
898
- }
899
-
900
- const attributeName = existingAttrNode ? (existingAttrNode.name as ts.Identifier) : factory.createIdentifier(xmlAttr.name);
901
- const existingProps =
902
- existingAttrNode && ts.isObjectLiteralExpression(existingAttrNode.initializer) ? [...existingAttrNode.initializer.properties] : [];
903
- const existingPropNames = new Set<string>();
904
-
905
- for (const p of existingProps) {
906
- if (p.name && ts.isIdentifier(p.name)) {
907
- existingPropNames.add(p.name.text);
908
- }
909
- }
910
-
911
- const propsToAdd: ts.PropertyAssignment[] = [];
912
-
913
- // Unified logic to build required properties
914
- if (xmlAttr.meta) {
915
- const meta = xmlAttr.meta.$;
916
-
917
- if (xmlAttr.client && !existingPropNames.has("client")) {
918
- propsToAdd.push(factory.createPropertyAssignment("client", factory.createTrue()));
919
- }
920
-
921
- if (meta.readable === "false" && !existingPropNames.has("read")) {
922
- propsToAdd.push(factory.createPropertyAssignment("read", factory.createFalse()));
923
- }
924
-
925
- if (meta.writable === "true" && !existingPropNames.has("write")) {
926
- propsToAdd.push(factory.createPropertyAssignment("write", factory.createTrue()));
927
- }
928
-
929
- if (meta.writeOptional === "true" && !existingPropNames.has("writeOptional")) {
930
- propsToAdd.push(factory.createPropertyAssignment("writeOptional", factory.createTrue()));
931
- }
932
-
933
- if (meta.reportRequired === "true" && !existingPropNames.has("report")) {
934
- propsToAdd.push(factory.createPropertyAssignment("report", factory.createTrue()));
935
- }
936
-
937
- if (meta.sceneRequired === "true" && !existingPropNames.has("scene")) {
938
- propsToAdd.push(factory.createPropertyAssignment("scene", factory.createTrue()));
939
- }
940
-
941
- if (meta.required === "true" && !existingPropNames.has("required")) {
942
- propsToAdd.push(factory.createPropertyAssignment("required", factory.createTrue()));
943
- }
944
-
945
- if (meta.min && !existingPropNames.has("min")) {
946
- // skip when too long for number format (not especially useful anyway for these types)
947
- if (xmlAttr.dataTypeExpr !== "DataType.IEEE_ADDR" && xmlAttr.dataTypeExpr !== "DataType.SEC_KEY") {
948
- propsToAdd.push(factory.createPropertyAssignment("min", createSafeNumericLiteral(meta.min, factory)));
949
- }
950
- }
951
-
952
- if (meta.max && !existingPropNames.has("max")) {
953
- // skip when too long for number format (not especially useful anyway for these types)
954
- if (xmlAttr.dataTypeExpr !== "DataType.IEEE_ADDR" && xmlAttr.dataTypeExpr !== "DataType.SEC_KEY") {
955
- propsToAdd.push(factory.createPropertyAssignment("max", createSafeNumericLiteral(meta.max, factory)));
956
- }
957
- }
958
-
959
- if (meta.default != null && !existingPropNames.has("default")) {
960
- if (isNumericDataType(xmlAttr.dataTypeExpr)) {
961
- propsToAdd.push(factory.createPropertyAssignment("default", createSafeNumericLiteral(meta.default, factory)));
962
- } else {
963
- propsToAdd.push(factory.createPropertyAssignment("default", factory.createStringLiteral(meta.default)));
964
- }
965
- }
966
-
967
- if (meta.defaultRef && !existingPropNames.has("defaultRef")) {
968
- const otherAttributes = xmlAttributes.filter((a) => a.id !== xmlAttr.id);
969
- const matched = findBestFuzzyAttrMatch(meta.defaultRef, otherAttributes, existingTsNames, factory);
970
-
971
- if (!matched) {
972
- console.log(`\x1b[31mCould not find match for attribute ${meta.defaultRef}.\x1b[0m`);
973
- }
974
-
975
- propsToAdd.push(
976
- factory.createPropertyAssignment(
977
- "defaultRef",
978
- matched ?? factory.createStringLiteral(normalizeAttributeOrCommandName(meta.defaultRef)),
979
- ),
980
- );
981
- }
982
- }
983
-
984
- if (xmlAttr.meta?.restriction?.[0]) {
985
- const restriction = xmlAttr.meta.restriction[0];
986
- const otherAttributes = xmlAttributes.filter((a) => a.id !== xmlAttr.id);
987
- const restrictionFacets: {name: string; value: string | {name: string; value: string}[] | undefined; isRef?: boolean}[] = [
988
- {name: "length", value: restriction["type:length"]?.[0]?.$?.value},
989
- {name: "minLen", value: restriction["type:minLength"]?.[0]?.$?.value},
990
- {name: "maxLen", value: restriction["type:maxLength"]?.[0]?.$?.value},
991
- {name: "minExcl", value: restriction["type:minExclusive"]?.[0]?.$?.value},
992
- {name: "min", value: restriction["type:minInclusive"]?.[0]?.$?.value},
993
- {name: "maxExcl", value: restriction["type:maxExclusive"]?.[0]?.$?.value},
994
- {name: "max", value: restriction["type:maxInclusive"]?.[0]?.$?.value},
995
- {name: "minRef", value: restriction["type:minInclusiveRef"]?.[0]?.$?.ref, isRef: true},
996
- {name: "minExclRef", value: restriction["type:minExclusiveRef"]?.[0]?.$?.ref, isRef: true},
997
- {name: "maxRef", value: restriction["type:maxInclusiveRef"]?.[0]?.$?.ref, isRef: true},
998
- {name: "maxExclRef", value: restriction["type:maxExclusiveRef"]?.[0]?.$?.ref, isRef: true},
999
- {name: "special", value: restriction["type:special"]?.map((s) => s.$)},
1000
- ];
1001
-
1002
- for (const facet of restrictionFacets) {
1003
- if (facet.value && !existingPropNames.has(facet.name)) {
1004
- if (facet.name === "special" && Array.isArray(facet.value)) {
1005
- const specialArray = factory.createArrayLiteralExpression(
1006
- facet.value.map((s) =>
1007
- factory.createArrayLiteralExpression([factory.createStringLiteral(s.name), factory.createStringLiteral(s.value)]),
1008
- ),
1009
- true,
1010
- );
1011
-
1012
- propsToAdd.push(factory.createPropertyAssignment(facet.name, specialArray));
1013
- } else if (typeof facet.value === "string") {
1014
- if (facet.isRef) {
1015
- const matched = findBestFuzzyAttrMatch(facet.value, otherAttributes, existingTsNames, factory);
1016
-
1017
- if (!matched) {
1018
- console.log(`\x1b[31mCould not find match for attribute ${facet.value}.\x1b[0m`);
1019
- }
1020
-
1021
- propsToAdd.push(
1022
- factory.createPropertyAssignment(
1023
- facet.name,
1024
- matched ?? factory.createStringLiteral(normalizeAttributeOrCommandName(facet.value)),
1025
- ),
1026
- );
1027
- } else {
1028
- if (facet.name === "minLength" && facet.value === "0") {
1029
- continue;
1030
- }
1031
-
1032
- propsToAdd.push(factory.createPropertyAssignment(facet.name, createSafeNumericLiteral(facet.value, factory)));
1033
- }
1034
- }
1035
- }
1036
- }
1037
- }
1038
-
1039
- let finalNode: ts.PropertyAssignment;
1040
-
1041
- if (existingAttrNode) {
1042
- processedTsNodes.add(existingAttrNode);
1043
- const finalProps = [...existingProps, ...propsToAdd];
1044
- const finalInitializer = factory.updateObjectLiteralExpression(existingAttrNode.initializer as ts.ObjectLiteralExpression, finalProps);
1045
- finalNode = factory.updatePropertyAssignment(existingAttrNode, attributeName, finalInitializer);
1046
- } else {
1047
- validation.addedAttributes.push(`0x${xmlAttr.id.toString(16).padStart(4, "0")} ${xmlAttr.name}`);
1048
- const typeIdentifierParts = xmlAttr.dataTypeExpr.split(".");
1049
- const typeIdentifier =
1050
- typeIdentifierParts.length > 1
1051
- ? factory.createPropertyAccessExpression(factory.createIdentifier(typeIdentifierParts[0]), typeIdentifierParts[1])
1052
- : factory.createIdentifier(xmlAttr.dataTypeExpr);
1053
- const finalProps = [
1054
- factory.createPropertyAssignment("ID", factory.createNumericLiteral(String(xmlAttr.id))),
1055
- factory.createPropertyAssignment("type", typeIdentifier),
1056
- ...propsToAdd,
1057
- ];
1058
- const finalInitializer = factory.createObjectLiteralExpression(finalProps, true);
1059
- finalNode = factory.createPropertyAssignment(attributeName, finalInitializer);
1060
- }
1061
-
1062
- finalAttributes.push(finalNode);
1063
- }
1064
-
1065
- // Add back any TS-only attributes that were not processed
1066
- for (const nodes of existingTsAttributes.values()) {
1067
- for (const node of nodes) {
1068
- if (!processedTsNodes.has(node)) {
1069
- finalAttributes.push(node);
1070
- }
1071
- }
1072
- }
1073
-
1074
- finalAttributes.sort((a, b) => {
1075
- const idA = extractNumericId(a) ?? -1;
1076
- const idB = extractNumericId(b) ?? -1;
1077
-
1078
- if (idA !== idB) {
1079
- return idA - idB;
1080
- }
1081
-
1082
- // If IDs are the same, check for manufacturerCode to sort standard attributes first
1083
- const initA = a.initializer as ts.ObjectLiteralExpression;
1084
- const initB = b.initializer as ts.ObjectLiteralExpression;
1085
- const hasManuA = findProperty(initA, "manufacturerCode") !== undefined;
1086
- const hasManuB = findProperty(initB, "manufacturerCode") !== undefined;
1087
-
1088
- if (hasManuA && !hasManuB) {
1089
- return 1;
1090
- }
1091
-
1092
- if (!hasManuA && hasManuB) {
1093
- return -1;
1094
- }
1095
-
1096
- return 0;
1097
- });
1098
-
1099
- const updatedInitializer = factory.updateObjectLiteralExpression(initializer, finalAttributes);
1100
-
1101
- return factory.updatePropertyAssignment(attributesProp, attributesProp.name, updatedInitializer);
1102
- }
1103
-
1104
- function updateCommands(
1105
- commandsProp: ts.PropertyAssignment,
1106
- xmlCommands: ParsedXMLCommand[],
1107
- xmlAttributes: ParsedXMLAttribute[],
1108
- existingTsAttributeNames: Map<number, string>,
1109
- isResponse: boolean,
1110
- factory: ts.NodeFactory,
1111
- validation: ValidationRecord,
1112
- ): ts.PropertyAssignment {
1113
- const initializer = commandsProp.initializer as ts.ObjectLiteralExpression;
1114
- const existingCommandIds = new Map<number, ts.PropertyAssignment[]>();
1115
-
1116
- for (const cmd of initializer.properties) {
1117
- if (ts.isPropertyAssignment(cmd)) {
1118
- const id = extractNumericId(cmd);
1119
-
1120
- if (id !== undefined) {
1121
- if (!existingCommandIds.has(id)) {
1122
- existingCommandIds.set(id, []);
1123
- }
1124
-
1125
- // biome-ignore lint/style/noNonNullAssertion: set above if not exist
1126
- existingCommandIds.get(id)!.push(cmd);
1127
- }
1128
- }
1129
- }
1130
-
1131
- const finalCommands: ts.PropertyAssignment[] = [];
1132
- const xmlCommandsForScope: ParsedXMLCommand[] = [];
1133
-
1134
- for (const cmd of xmlCommands) {
1135
- if (cmd.isResponse === isResponse) {
1136
- xmlCommandsForScope.push(cmd);
1137
- }
1138
- }
1139
-
1140
- const processedTsNodes = new Set<ts.PropertyAssignment>();
1141
-
1142
- for (const xmlCmd of xmlCommandsForScope) {
1143
- const existingCmdNodes = existingCommandIds.get(xmlCmd.id);
1144
- let existingCmdNode: ts.PropertyAssignment | undefined;
1145
-
1146
- if (existingCmdNodes) {
1147
- if (existingCmdNodes.length === 1) {
1148
- existingCmdNode = existingCmdNodes[0];
1149
- } else if (existingCmdNodes.length > 1) {
1150
- let bestMatch: {score: number; node: ts.PropertyAssignment | undefined} = {score: -1, node: undefined};
1151
-
1152
- for (const node of existingCmdNodes) {
1153
- const nodeName = ts.isIdentifier(node.name) ? node.name.text : "";
1154
- const score = fuzzyMatch(xmlCmd.name, nodeName);
1155
-
1156
- if (score > bestMatch.score) {
1157
- bestMatch = {score, node};
1158
- }
1159
- }
1160
-
1161
- existingCmdNode = bestMatch.node;
1162
- }
1163
- }
1164
-
1165
- const commandName = existingCmdNode ? existingCmdNode.name : factory.createIdentifier(xmlCmd.name);
1166
- const existingCmdProps =
1167
- existingCmdNode && ts.isObjectLiteralExpression(existingCmdNode.initializer) ? [...existingCmdNode.initializer.properties] : [];
1168
- const existingCmdPropNames = new Set<string>();
1169
-
1170
- for (const p of existingCmdProps) {
1171
- if (p.name && ts.isIdentifier(p.name)) {
1172
- existingCmdPropNames.add(p.name.text);
1173
- }
1174
- }
1175
-
1176
- // --- Unified Parameter Logic ---
1177
- const paramsProp = existingCmdNode ? findProperty(existingCmdNode.initializer as ts.ObjectLiteralExpression, "parameters") : undefined;
1178
- const existingParamsArray: ts.ObjectLiteralExpression[] = [];
1179
- const existingTsParamNames = new Map<string, string>(); // Map from normalized XML name to TS name
1180
-
1181
- if (paramsProp && ts.isArrayLiteralExpression(paramsProp.initializer)) {
1182
- for (const [i, el] of paramsProp.initializer.elements.entries()) {
1183
- if (ts.isObjectLiteralExpression(el)) {
1184
- existingParamsArray.push(el);
1185
- const nameProp = findProperty(el, "name");
1186
-
1187
- if (nameProp && ts.isStringLiteral(nameProp.initializer) && xmlCmd.parameters[i]) {
1188
- existingTsParamNames.set(xmlCmd.parameters[i].name, nameProp.initializer.text);
1189
- }
1190
- }
1191
- }
1192
- }
1193
-
1194
- const newParams: ts.ObjectLiteralExpression[] = [];
1195
- const maxParams = Math.max(xmlCmd.parameters.length, existingParamsArray.length);
1196
-
1197
- for (let i = 0; i < maxParams; i++) {
1198
- const xmlParam = xmlCmd.parameters[i];
1199
- const existingParamExpr = existingParamsArray[i];
1200
-
1201
- if (!xmlParam && existingParamExpr) {
1202
- newParams.push(existingParamExpr);
1203
-
1204
- continue;
1205
- }
1206
-
1207
- if (!xmlParam) {
1208
- continue;
1209
- }
1210
-
1211
- const existingParamProps = existingParamExpr ? [...existingParamExpr.properties] : [];
1212
- const existingParamPropNames = new Set(existingParamProps.flatMap((p) => (p.name && ts.isIdentifier(p.name) ? [p.name.text] : [])));
1213
- const paramPropsToAdd: ts.PropertyAssignment[] = [];
1214
- const otherParams = xmlCmd.parameters.filter((_p, index) => index !== i);
1215
-
1216
- // Add name/type if missing
1217
- if (!existingParamPropNames.has("name")) {
1218
- paramPropsToAdd.push(factory.createPropertyAssignment("name", factory.createStringLiteral(xmlParam.name)));
1219
- }
1220
-
1221
- if (!existingParamPropNames.has("type")) {
1222
- const typeIdentifierParts = xmlParam.dataTypeExpr.split(".");
1223
- const typeIdentifier =
1224
- typeIdentifierParts.length > 1
1225
- ? factory.createPropertyAccessExpression(factory.createIdentifier(typeIdentifierParts[0]), typeIdentifierParts[1])
1226
- : factory.createIdentifier(xmlParam.dataTypeExpr);
1227
-
1228
- paramPropsToAdd.push(factory.createPropertyAssignment("type", typeIdentifier));
1229
- }
1230
-
1231
- // Add metadata
1232
- if (xmlParam.meta?.$.arrayLengthSize && !existingParamPropNames.has("arrayLengthSize")) {
1233
- paramPropsToAdd.push(
1234
- factory.createPropertyAssignment("arrayLengthSize", factory.createNumericLiteral(xmlParam.meta.$.arrayLengthSize)),
1235
- );
1236
- }
1237
-
1238
- if (xmlParam.meta?.$.arrayLengthField && !existingParamPropNames.has("arrayLengthField")) {
1239
- const matched = findBestFuzzyParamMatch(xmlParam.meta.$.arrayLengthField, otherParams, existingTsParamNames, factory);
1240
-
1241
- if (!matched) {
1242
- console.log(`\x1b[31mCould not find match for parameter ${xmlParam.meta.$.arrayLengthField}.\x1b[0m`);
1243
- }
1244
-
1245
- paramPropsToAdd.push(
1246
- factory.createPropertyAssignment(
1247
- "arrayLengthField",
1248
- matched ?? factory.createStringLiteral(normalizeAttributeOrCommandName(xmlParam.meta.$.arrayLengthField)),
1249
- ),
1250
- );
1251
- }
1252
-
1253
- // Add all restrictions ("..Count" params don't have metas)
1254
- if (xmlParam.meta?.restriction?.[0] && !xmlParam.isLength) {
1255
- const restriction = xmlParam.meta.restriction[0];
1256
- const restrictionFacets: {name: string; value: string | {name: string; value: string}[] | undefined; isRef?: boolean}[] = [
1257
- {name: "length", value: restriction["type:length"]?.[0]?.$?.value},
1258
- {name: "minLen", value: restriction["type:minLength"]?.[0]?.$?.value},
1259
- {name: "maxLen", value: restriction["type:maxLength"]?.[0]?.$?.value},
1260
- {name: "minExcl", value: restriction["type:minExclusive"]?.[0]?.$?.value},
1261
- {name: "min", value: restriction["type:minInclusive"]?.[0]?.$?.value},
1262
- {name: "maxExcl", value: restriction["type:maxExclusive"]?.[0]?.$?.value},
1263
- {name: "max", value: restriction["type:maxInclusive"]?.[0]?.$?.value},
1264
- {name: "minRef", value: restriction["type:minInclusiveRef"]?.[0]?.$?.ref, isRef: true},
1265
- {name: "minExclRef", value: restriction["type:minExclusiveRef"]?.[0]?.$?.ref, isRef: true},
1266
- {name: "maxRef", value: restriction["type:maxInclusiveRef"]?.[0]?.$?.ref, isRef: true},
1267
- {name: "maxExclRef", value: restriction["type:maxExclusiveRef"]?.[0]?.$?.ref, isRef: true},
1268
- {name: "special", value: restriction["type:special"]?.map((s) => s.$)},
1269
- ];
1270
-
1271
- for (const facet of restrictionFacets) {
1272
- if (facet.value && !existingParamPropNames.has(facet.name)) {
1273
- if (facet.name === "special" && Array.isArray(facet.value)) {
1274
- const specialArray = factory.createArrayLiteralExpression(
1275
- facet.value.map((s) =>
1276
- factory.createArrayLiteralExpression([factory.createStringLiteral(s.name), factory.createStringLiteral(s.value)]),
1277
- ),
1278
- true,
1279
- );
1280
- paramPropsToAdd.push(factory.createPropertyAssignment(facet.name, specialArray));
1281
- } else if (typeof facet.value === "string") {
1282
- if (facet.isRef) {
1283
- let matched = findBestFuzzyAttrMatch(facet.value, xmlAttributes, existingTsAttributeNames, factory);
1284
-
1285
- if (!matched) {
1286
- matched = findBestFuzzyParamMatch(facet.value, otherParams, existingTsParamNames, factory);
1287
- }
1288
-
1289
- if (!matched) {
1290
- console.log(`\x1b[31mCould not find match for ref ${facet.value}.\x1b[0m`);
1291
- }
1292
-
1293
- paramPropsToAdd.push(
1294
- factory.createPropertyAssignment(
1295
- facet.name,
1296
- matched ?? factory.createStringLiteral(normalizeAttributeOrCommandName(facet.value)),
1297
- ),
1298
- );
1299
- } else {
1300
- // a few entries are in hex form like "fd", this is obviously flawed, entries that are too complex should be overriden instead
1301
- if (facet.value.match(/^[0-9]+$/)) {
1302
- paramPropsToAdd.push(
1303
- factory.createPropertyAssignment(facet.name, createSafeNumericLiteral(facet.value, factory)),
1304
- );
1305
- } else if (facet.value.match(/^[0-9a-fA-F]+$/)) {
1306
- console.log(`\x1b[33mWriting ${JSON.stringify(facet)} as hex number\x1b[0m`);
1307
- paramPropsToAdd.push(
1308
- factory.createPropertyAssignment(facet.name, createSafeNumericLiteral(`0x${facet.value}`, factory)),
1309
- );
1310
- }
1311
- }
1312
- }
1313
- }
1314
- }
1315
- }
1316
-
1317
- if (existingParamExpr) {
1318
- newParams.push(factory.updateObjectLiteralExpression(existingParamExpr, [...existingParamProps, ...paramPropsToAdd]));
1319
- } else {
1320
- validation.addedParameters.push(`0x${xmlCmd.id.toString(16).padStart(2, "0")} ${xmlCmd.name} > ${xmlParam.name}`);
1321
- newParams.push(factory.createObjectLiteralExpression(paramPropsToAdd, true));
1322
- }
1323
- }
1324
- // --- End Unified Parameter Logic ---
1325
-
1326
- const cmdPropsToAdd: ts.PropertyAssignment[] = [];
1327
-
1328
- cmdPropsToAdd.push(factory.createPropertyAssignment("parameters", factory.createArrayLiteralExpression(newParams, true)));
1329
-
1330
- if (xmlCmd.meta?.$.required === "true" && !existingCmdPropNames.has("required")) {
1331
- cmdPropsToAdd.push(factory.createPropertyAssignment("required", factory.createTrue()));
1332
- }
1333
-
1334
- let finalNode: ts.PropertyAssignment;
1335
-
1336
- if (existingCmdNode) {
1337
- processedTsNodes.add(existingCmdNode);
1338
-
1339
- const finalProps = [...existingCmdProps];
1340
-
1341
- for (const propToAdd of cmdPropsToAdd) {
1342
- const propName = (propToAdd.name as ts.Identifier).text;
1343
- const existingIndex = finalProps.findIndex((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === propName);
1344
-
1345
- if (existingIndex !== -1) {
1346
- finalProps[existingIndex] = propToAdd;
1347
- } else {
1348
- finalProps.push(propToAdd);
1349
- }
1350
- }
1351
-
1352
- const finalInitializer = factory.updateObjectLiteralExpression(existingCmdNode.initializer as ts.ObjectLiteralExpression, finalProps);
1353
- finalNode = factory.updatePropertyAssignment(existingCmdNode, commandName, finalInitializer);
1354
- } else {
1355
- const hexId = `0x${xmlCmd.id.toString(16).padStart(2, "0")}`;
1356
-
1357
- validation.addedCommands.push(`${hexId} ${xmlCmd.name}`);
1358
-
1359
- const finalProps = [factory.createPropertyAssignment("ID", factory.createNumericLiteral(hexId)), ...cmdPropsToAdd];
1360
- const finalInitializer = factory.createObjectLiteralExpression(finalProps, true);
1361
- finalNode = factory.createPropertyAssignment(commandName, finalInitializer);
1362
- }
1363
-
1364
- finalCommands.push(finalNode);
1365
- }
1366
-
1367
- // Add back any TS-only commands
1368
- for (const tsOnlyCmds of existingCommandIds.values()) {
1369
- for (const tsOnlyCmd of tsOnlyCmds) {
1370
- if (!processedTsNodes.has(tsOnlyCmd)) {
1371
- finalCommands.push(tsOnlyCmd);
1372
- }
1373
- }
1374
- }
1375
-
1376
- finalCommands.sort((a, b) => {
1377
- const idA = extractNumericId(a) ?? -1;
1378
- const idB = extractNumericId(b) ?? -1;
1379
-
1380
- return idA - idB;
1381
- });
1382
-
1383
- const updatedInitializer = factory.updateObjectLiteralExpression(initializer, finalCommands);
1384
-
1385
- return factory.updatePropertyAssignment(commandsProp, commandsProp.name, updatedInitializer);
1386
- }
1387
-
1388
- // #endregion
1389
-
1390
- // #region Main
1391
-
1392
- async function main(): Promise<void> {
1393
- const args = process.argv.slice(2);
1394
-
1395
- if (args.length !== 1) {
1396
- throw new Error("Usage: tsx scripts/zap-update-clusters.ts <path-to-xml-files>");
1397
- }
1398
-
1399
- const xmlPath = args[0];
1400
- const clusterFile = "src/zspec/zcl/definition/cluster.ts";
1401
- const overridesFile = "scripts/zap-xml-clusters-overrides-data.ts";
1402
- const validation: ValidationRecord = {
1403
- warnings: [],
1404
- errors: [],
1405
- unknownTypes: new Set(),
1406
- addedAttributes: [],
1407
- addedCommands: [],
1408
- addedParameters: [],
1409
- scannedClusters: [],
1410
- changedClusters: [],
1411
- xmlClustersMissingFromTs: new Map(),
1412
- tsClustersMissingFromXml: [],
1413
- };
1414
- const allXmlClusters: XMLCluster[] = [];
1415
-
1416
- const globalResolver = new TypeResolver();
1417
- const libraryPath = path.join(xmlPath, "library.xml");
1418
-
1419
- try {
1420
- // Collect all data and types first.
1421
- const {includeFiles, libraryTypes} = await collectFromLibrary(libraryPath);
1422
-
1423
- globalResolver.add(libraryTypes); // Populate the global resolver.
1424
-
1425
- for (const f of includeFiles) {
1426
- if (!f.toLowerCase().endsWith(".xml")) {
1427
- continue;
1428
- }
1429
- try {
1430
- const clusters = await parseXMLClusters(f);
1431
- allXmlClusters.push(...clusters);
1432
- } catch (e) {
1433
- validation.errors.push(`Failed parsing XML ${f}: ${(e as Error).message}`);
1434
- }
1435
- }
1436
- } catch (e) {
1437
- throw new Error(`Unable to process library file at ${libraryPath}: ${(e as Error).message}`);
1438
- }
1439
-
1440
- const overridesFileContent = await fs.readFile(overridesFile, "utf8");
1441
- const overridesSourceFile = ts.createSourceFile(overridesFile, overridesFileContent, ts.ScriptTarget.Latest, true);
1442
- const overrides = parseOverrides(overridesSourceFile);
1443
-
1444
- applyXmlOverrides(allXmlClusters, overrides);
1445
-
1446
- // Pass the prepared globalResolver to the parsing function.
1447
- const parsedData = parseClustersFromXML(allXmlClusters, globalResolver, validation);
1448
- const xmlClusterDataMap = new Map<number, ParsedXMLClusterData>();
1449
-
1450
- for (const d of parsedData) {
1451
- const existing = xmlClusterDataMap.get(d.id);
1452
-
1453
- if (existing) {
1454
- // This is a derived cluster, merge its data with the base cluster.
1455
- existing.attributes.push(...d.attributes);
1456
- existing.serverCommands.push(...d.serverCommands);
1457
- existing.clientCommands.push(...d.clientCommands);
1458
- } else {
1459
- xmlClusterDataMap.set(d.id, d);
1460
- }
1461
- }
1462
-
1463
- const clusterFileContent = await fs.readFile(clusterFile, "utf8");
1464
- const sourceFile = ts.createSourceFile(clusterFile, clusterFileContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1465
- const updateTransformer = createUpdateTransformer(xmlClusterDataMap, validation);
1466
- const hexTransformer = createHexIdTransformer();
1467
- const result1 = ts.transform(sourceFile, [updateTransformer]);
1468
- const updatedSourceFile = result1.transformed[0];
1469
- const result3 = ts.transform(updatedSourceFile, [hexTransformer]);
1470
- const finalSourceFile = result3.transformed[0];
1471
- const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed, removeComments: false});
1472
- let newContent = printer.printFile(finalSourceFile);
1473
- // help biome re-format
1474
- newContent = newContent.replaceAll("{\n ID:", "{ID:");
1475
- newContent = newContent.replaceAll("{\n name:", "{name:");
1476
-
1477
- await fs.writeFile(clusterFile, newContent, "utf8");
1478
-
1479
- console.log(
1480
- `Successfully updated ${clusterFile}. Changes: ${validation.changedClusters} clusters, ${validation.addedAttributes} attributes, ${validation.addedCommands} commands, ${validation.addedParameters} parameters.`,
1481
- );
1482
-
1483
- if (validation.warnings.length > 0) {
1484
- console.log("Warnings:");
1485
-
1486
- for (const w of validation.warnings) {
1487
- console.log(` - ${w}`);
1488
- }
1489
- }
1490
-
1491
- if (validation.errors.length > 0) {
1492
- console.log("Errors:");
1493
-
1494
- for (const e of validation.errors) {
1495
- console.log(` - ${e}`);
1496
- }
1497
- }
1498
-
1499
- const report = {
1500
- scannedClusters: validation.scannedClusters,
1501
- changedClusters: validation.changedClusters,
1502
- addedAttributes: validation.addedAttributes,
1503
- addedCommands: validation.addedCommands,
1504
- addedParameters: validation.addedParameters,
1505
- unknownTypes: Array.from(validation.unknownTypes.values()),
1506
- xmlClustersMissingFromTs: Array.from(validation.xmlClustersMissingFromTs.values()),
1507
- tsClustersMissingFromXml: validation.tsClustersMissingFromXml,
1508
- warnings: validation.warnings,
1509
- errors: validation.errors,
1510
- };
1511
-
1512
- await fs.writeFile("scripts/zap-update-clusters-report.json", JSON.stringify(report, null, 2), "utf8");
1513
- }
1514
-
1515
- main().catch((err) => {
1516
- console.error(err);
1517
- process.exit(1);
1518
- });
1519
-
1520
- // #endregion