matterbridge 3.4.3-dev-20251209-e6cb85f → 3.4.3

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 (324) hide show
  1. package/README.md +2 -3
  2. package/dist/broadcastServer.d.ts +144 -0
  3. package/dist/broadcastServer.d.ts.map +1 -0
  4. package/dist/broadcastServer.js +119 -0
  5. package/dist/broadcastServer.js.map +1 -0
  6. package/dist/broadcastServerTypes.d.ts +841 -0
  7. package/dist/broadcastServerTypes.d.ts.map +1 -0
  8. package/dist/broadcastServerTypes.js +24 -0
  9. package/dist/broadcastServerTypes.js.map +1 -0
  10. package/dist/cli.d.ts +30 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +97 -1
  13. package/dist/cli.js.map +1 -0
  14. package/dist/cliEmitter.d.ts +50 -0
  15. package/dist/cliEmitter.d.ts.map +1 -0
  16. package/dist/cliEmitter.js +37 -0
  17. package/dist/cliEmitter.js.map +1 -0
  18. package/dist/cliHistory.d.ts +48 -0
  19. package/dist/cliHistory.d.ts.map +1 -0
  20. package/dist/cliHistory.js +38 -0
  21. package/dist/cliHistory.js.map +1 -0
  22. package/dist/clusters/export.d.ts +2 -0
  23. package/dist/clusters/export.d.ts.map +1 -0
  24. package/dist/clusters/export.js +2 -0
  25. package/dist/clusters/export.js.map +1 -0
  26. package/dist/deviceManager.d.ts +135 -0
  27. package/dist/deviceManager.d.ts.map +1 -0
  28. package/dist/deviceManager.js +113 -1
  29. package/dist/deviceManager.js.map +1 -0
  30. package/dist/devices/airConditioner.d.ts +98 -0
  31. package/dist/devices/airConditioner.d.ts.map +1 -0
  32. package/dist/devices/airConditioner.js +57 -0
  33. package/dist/devices/airConditioner.js.map +1 -0
  34. package/dist/devices/batteryStorage.d.ts +48 -0
  35. package/dist/devices/batteryStorage.d.ts.map +1 -0
  36. package/dist/devices/batteryStorage.js +48 -1
  37. package/dist/devices/batteryStorage.js.map +1 -0
  38. package/dist/devices/cooktop.d.ts +61 -0
  39. package/dist/devices/cooktop.d.ts.map +1 -0
  40. package/dist/devices/cooktop.js +56 -0
  41. package/dist/devices/cooktop.js.map +1 -0
  42. package/dist/devices/dishwasher.d.ts +71 -0
  43. package/dist/devices/dishwasher.d.ts.map +1 -0
  44. package/dist/devices/dishwasher.js +57 -0
  45. package/dist/devices/dishwasher.js.map +1 -0
  46. package/dist/devices/evse.d.ts +76 -0
  47. package/dist/devices/evse.d.ts.map +1 -0
  48. package/dist/devices/evse.js +74 -10
  49. package/dist/devices/evse.js.map +1 -0
  50. package/dist/devices/export.d.ts +17 -0
  51. package/dist/devices/export.d.ts.map +1 -0
  52. package/dist/devices/export.js +5 -0
  53. package/dist/devices/export.js.map +1 -0
  54. package/dist/devices/extractorHood.d.ts +46 -0
  55. package/dist/devices/extractorHood.d.ts.map +1 -0
  56. package/dist/devices/extractorHood.js +43 -0
  57. package/dist/devices/extractorHood.js.map +1 -0
  58. package/dist/devices/heatPump.d.ts +47 -0
  59. package/dist/devices/heatPump.d.ts.map +1 -0
  60. package/dist/devices/heatPump.js +50 -2
  61. package/dist/devices/heatPump.js.map +1 -0
  62. package/dist/devices/laundryDryer.d.ts +67 -0
  63. package/dist/devices/laundryDryer.d.ts.map +1 -0
  64. package/dist/devices/laundryDryer.js +62 -3
  65. package/dist/devices/laundryDryer.js.map +1 -0
  66. package/dist/devices/laundryWasher.d.ts +81 -0
  67. package/dist/devices/laundryWasher.d.ts.map +1 -0
  68. package/dist/devices/laundryWasher.js +70 -4
  69. package/dist/devices/laundryWasher.js.map +1 -0
  70. package/dist/devices/microwaveOven.d.ts +168 -0
  71. package/dist/devices/microwaveOven.d.ts.map +1 -0
  72. package/dist/devices/microwaveOven.js +88 -5
  73. package/dist/devices/microwaveOven.js.map +1 -0
  74. package/dist/devices/oven.d.ts +105 -0
  75. package/dist/devices/oven.d.ts.map +1 -0
  76. package/dist/devices/oven.js +85 -0
  77. package/dist/devices/oven.js.map +1 -0
  78. package/dist/devices/refrigerator.d.ts +118 -0
  79. package/dist/devices/refrigerator.d.ts.map +1 -0
  80. package/dist/devices/refrigerator.js +102 -0
  81. package/dist/devices/refrigerator.js.map +1 -0
  82. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  83. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  84. package/dist/devices/roboticVacuumCleaner.js +100 -9
  85. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  86. package/dist/devices/solarPower.d.ts +40 -0
  87. package/dist/devices/solarPower.d.ts.map +1 -0
  88. package/dist/devices/solarPower.js +38 -0
  89. package/dist/devices/solarPower.js.map +1 -0
  90. package/dist/devices/speaker.d.ts +87 -0
  91. package/dist/devices/speaker.d.ts.map +1 -0
  92. package/dist/devices/speaker.js +84 -0
  93. package/dist/devices/speaker.js.map +1 -0
  94. package/dist/devices/temperatureControl.d.ts +166 -0
  95. package/dist/devices/temperatureControl.d.ts.map +1 -0
  96. package/dist/devices/temperatureControl.js +24 -3
  97. package/dist/devices/temperatureControl.js.map +1 -0
  98. package/dist/devices/waterHeater.d.ts +111 -0
  99. package/dist/devices/waterHeater.d.ts.map +1 -0
  100. package/dist/devices/waterHeater.js +82 -2
  101. package/dist/devices/waterHeater.js.map +1 -0
  102. package/dist/dgram/coap.d.ts +205 -0
  103. package/dist/dgram/coap.d.ts.map +1 -0
  104. package/dist/dgram/coap.js +126 -13
  105. package/dist/dgram/coap.js.map +1 -0
  106. package/dist/dgram/dgram.d.ts +141 -0
  107. package/dist/dgram/dgram.d.ts.map +1 -0
  108. package/dist/dgram/dgram.js +114 -2
  109. package/dist/dgram/dgram.js.map +1 -0
  110. package/dist/dgram/mb_coap.d.ts +24 -0
  111. package/dist/dgram/mb_coap.d.ts.map +1 -0
  112. package/dist/dgram/mb_coap.js +41 -3
  113. package/dist/dgram/mb_coap.js.map +1 -0
  114. package/dist/dgram/mb_mdns.d.ts +24 -0
  115. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  116. package/dist/dgram/mb_mdns.js +80 -15
  117. package/dist/dgram/mb_mdns.js.map +1 -0
  118. package/dist/dgram/mdns.d.ts +290 -0
  119. package/dist/dgram/mdns.d.ts.map +1 -0
  120. package/dist/dgram/mdns.js +299 -137
  121. package/dist/dgram/mdns.js.map +1 -0
  122. package/dist/dgram/multicast.d.ts +67 -0
  123. package/dist/dgram/multicast.d.ts.map +1 -0
  124. package/dist/dgram/multicast.js +62 -1
  125. package/dist/dgram/multicast.js.map +1 -0
  126. package/dist/dgram/unicast.d.ts +56 -0
  127. package/dist/dgram/unicast.d.ts.map +1 -0
  128. package/dist/dgram/unicast.js +54 -0
  129. package/dist/dgram/unicast.js.map +1 -0
  130. package/dist/frontend.d.ts +238 -0
  131. package/dist/frontend.d.ts.map +1 -0
  132. package/dist/frontend.js +455 -35
  133. package/dist/frontend.js.map +1 -0
  134. package/dist/frontendTypes.d.ts +529 -0
  135. package/dist/frontendTypes.d.ts.map +1 -0
  136. package/dist/frontendTypes.js +45 -0
  137. package/dist/frontendTypes.js.map +1 -0
  138. package/dist/helpers.d.ts +48 -0
  139. package/dist/helpers.d.ts.map +1 -0
  140. package/dist/helpers.js +53 -0
  141. package/dist/helpers.js.map +1 -0
  142. package/dist/index.d.ts +34 -0
  143. package/dist/index.d.ts.map +1 -0
  144. package/dist/index.js +25 -0
  145. package/dist/index.js.map +1 -0
  146. package/dist/jestutils/export.d.ts +2 -0
  147. package/dist/jestutils/export.d.ts.map +1 -0
  148. package/dist/jestutils/export.js +1 -0
  149. package/dist/jestutils/export.js.map +1 -0
  150. package/dist/jestutils/jestHelpers.d.ts +345 -0
  151. package/dist/jestutils/jestHelpers.d.ts.map +1 -0
  152. package/dist/jestutils/jestHelpers.js +371 -14
  153. package/dist/jestutils/jestHelpers.js.map +1 -0
  154. package/dist/logger/export.d.ts +2 -0
  155. package/dist/logger/export.d.ts.map +1 -0
  156. package/dist/logger/export.js +1 -0
  157. package/dist/logger/export.js.map +1 -0
  158. package/dist/matter/behaviors.d.ts +2 -0
  159. package/dist/matter/behaviors.d.ts.map +1 -0
  160. package/dist/matter/behaviors.js +2 -0
  161. package/dist/matter/behaviors.js.map +1 -0
  162. package/dist/matter/clusters.d.ts +2 -0
  163. package/dist/matter/clusters.d.ts.map +1 -0
  164. package/dist/matter/clusters.js +2 -0
  165. package/dist/matter/clusters.js.map +1 -0
  166. package/dist/matter/devices.d.ts +2 -0
  167. package/dist/matter/devices.d.ts.map +1 -0
  168. package/dist/matter/devices.js +2 -0
  169. package/dist/matter/devices.js.map +1 -0
  170. package/dist/matter/endpoints.d.ts +2 -0
  171. package/dist/matter/endpoints.d.ts.map +1 -0
  172. package/dist/matter/endpoints.js +2 -0
  173. package/dist/matter/endpoints.js.map +1 -0
  174. package/dist/matter/export.d.ts +5 -0
  175. package/dist/matter/export.d.ts.map +1 -0
  176. package/dist/matter/export.js +3 -0
  177. package/dist/matter/export.js.map +1 -0
  178. package/dist/matter/types.d.ts +3 -0
  179. package/dist/matter/types.d.ts.map +1 -0
  180. package/dist/matter/types.js +3 -0
  181. package/dist/matter/types.js.map +1 -0
  182. package/dist/matterNode.d.ts +342 -0
  183. package/dist/matterNode.d.ts.map +1 -0
  184. package/dist/matterNode.js +369 -8
  185. package/dist/matterNode.js.map +1 -0
  186. package/dist/matterbridge.d.ts +492 -0
  187. package/dist/matterbridge.d.ts.map +1 -0
  188. package/dist/matterbridge.js +811 -46
  189. package/dist/matterbridge.js.map +1 -0
  190. package/dist/matterbridgeAccessoryPlatform.d.ts +41 -0
  191. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  192. package/dist/matterbridgeAccessoryPlatform.js +38 -0
  193. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  194. package/dist/matterbridgeBehaviors.d.ts +2404 -0
  195. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  196. package/dist/matterbridgeBehaviors.js +68 -5
  197. package/dist/matterbridgeBehaviors.js.map +1 -0
  198. package/dist/matterbridgeDeviceTypes.d.ts +698 -0
  199. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  200. package/dist/matterbridgeDeviceTypes.js +635 -14
  201. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  202. package/dist/matterbridgeDynamicPlatform.d.ts +41 -0
  203. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  204. package/dist/matterbridgeDynamicPlatform.js +38 -0
  205. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  206. package/dist/matterbridgeEndpoint.d.ts +1507 -0
  207. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  208. package/dist/matterbridgeEndpoint.js +1444 -53
  209. package/dist/matterbridgeEndpoint.js.map +1 -0
  210. package/dist/matterbridgeEndpointHelpers.d.ts +787 -0
  211. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  212. package/dist/matterbridgeEndpointHelpers.js +483 -20
  213. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  214. package/dist/matterbridgeEndpointTypes.d.ts +166 -0
  215. package/dist/matterbridgeEndpointTypes.d.ts.map +1 -0
  216. package/dist/matterbridgeEndpointTypes.js +25 -0
  217. package/dist/matterbridgeEndpointTypes.js.map +1 -0
  218. package/dist/matterbridgePlatform.d.ts +539 -0
  219. package/dist/matterbridgePlatform.d.ts.map +1 -0
  220. package/dist/matterbridgePlatform.js +451 -1
  221. package/dist/matterbridgePlatform.js.map +1 -0
  222. package/dist/matterbridgeTypes.d.ts +251 -0
  223. package/dist/matterbridgeTypes.d.ts.map +1 -0
  224. package/dist/matterbridgeTypes.js +26 -0
  225. package/dist/matterbridgeTypes.js.map +1 -0
  226. package/dist/pluginManager.d.ts +372 -0
  227. package/dist/pluginManager.d.ts.map +1 -0
  228. package/dist/pluginManager.js +341 -5
  229. package/dist/pluginManager.js.map +1 -0
  230. package/dist/shelly.d.ts +181 -0
  231. package/dist/shelly.d.ts.map +1 -0
  232. package/dist/shelly.js +178 -7
  233. package/dist/shelly.js.map +1 -0
  234. package/dist/storage/export.d.ts +2 -0
  235. package/dist/storage/export.d.ts.map +1 -0
  236. package/dist/storage/export.js +1 -0
  237. package/dist/storage/export.js.map +1 -0
  238. package/dist/update.d.ts +84 -0
  239. package/dist/update.d.ts.map +1 -0
  240. package/dist/update.js +93 -1
  241. package/dist/update.js.map +1 -0
  242. package/dist/utils/colorUtils.d.ts +101 -0
  243. package/dist/utils/colorUtils.d.ts.map +1 -0
  244. package/dist/utils/colorUtils.js +97 -2
  245. package/dist/utils/colorUtils.js.map +1 -0
  246. package/dist/utils/commandLine.d.ts +66 -0
  247. package/dist/utils/commandLine.d.ts.map +1 -0
  248. package/dist/utils/commandLine.js +60 -0
  249. package/dist/utils/commandLine.js.map +1 -0
  250. package/dist/utils/copyDirectory.d.ts +35 -0
  251. package/dist/utils/copyDirectory.d.ts.map +1 -0
  252. package/dist/utils/copyDirectory.js +37 -0
  253. package/dist/utils/copyDirectory.js.map +1 -0
  254. package/dist/utils/createDirectory.d.ts +34 -0
  255. package/dist/utils/createDirectory.d.ts.map +1 -0
  256. package/dist/utils/createDirectory.js +33 -0
  257. package/dist/utils/createDirectory.js.map +1 -0
  258. package/dist/utils/createZip.d.ts +39 -0
  259. package/dist/utils/createZip.d.ts.map +1 -0
  260. package/dist/utils/createZip.js +47 -2
  261. package/dist/utils/createZip.js.map +1 -0
  262. package/dist/utils/deepCopy.d.ts +32 -0
  263. package/dist/utils/deepCopy.d.ts.map +1 -0
  264. package/dist/utils/deepCopy.js +39 -0
  265. package/dist/utils/deepCopy.js.map +1 -0
  266. package/dist/utils/deepEqual.d.ts +54 -0
  267. package/dist/utils/deepEqual.d.ts.map +1 -0
  268. package/dist/utils/deepEqual.js +72 -1
  269. package/dist/utils/deepEqual.js.map +1 -0
  270. package/dist/utils/error.d.ts +45 -0
  271. package/dist/utils/error.d.ts.map +1 -0
  272. package/dist/utils/error.js +42 -0
  273. package/dist/utils/error.js.map +1 -0
  274. package/dist/utils/export.d.ts +13 -0
  275. package/dist/utils/export.d.ts.map +1 -0
  276. package/dist/utils/export.js +1 -0
  277. package/dist/utils/export.js.map +1 -0
  278. package/dist/utils/format.d.ts +53 -0
  279. package/dist/utils/format.d.ts.map +1 -0
  280. package/dist/utils/format.js +49 -0
  281. package/dist/utils/format.js.map +1 -0
  282. package/dist/utils/hex.d.ts +89 -0
  283. package/dist/utils/hex.d.ts.map +1 -0
  284. package/dist/utils/hex.js +124 -0
  285. package/dist/utils/hex.js.map +1 -0
  286. package/dist/utils/inspector.d.ts +87 -0
  287. package/dist/utils/inspector.d.ts.map +1 -0
  288. package/dist/utils/inspector.js +69 -1
  289. package/dist/utils/inspector.js.map +1 -0
  290. package/dist/utils/isvalid.d.ts +103 -0
  291. package/dist/utils/isvalid.d.ts.map +1 -0
  292. package/dist/utils/isvalid.js +101 -0
  293. package/dist/utils/isvalid.js.map +1 -0
  294. package/dist/utils/network.d.ts +111 -0
  295. package/dist/utils/network.d.ts.map +1 -0
  296. package/dist/utils/network.js +96 -5
  297. package/dist/utils/network.js.map +1 -0
  298. package/dist/utils/spawn.d.ts +33 -0
  299. package/dist/utils/spawn.d.ts.map +1 -0
  300. package/dist/utils/spawn.js +71 -1
  301. package/dist/utils/spawn.js.map +1 -0
  302. package/dist/utils/tracker.d.ts +108 -0
  303. package/dist/utils/tracker.d.ts.map +1 -0
  304. package/dist/utils/tracker.js +64 -1
  305. package/dist/utils/tracker.js.map +1 -0
  306. package/dist/utils/wait.d.ts +54 -0
  307. package/dist/utils/wait.d.ts.map +1 -0
  308. package/dist/utils/wait.js +60 -8
  309. package/dist/utils/wait.js.map +1 -0
  310. package/dist/workerGlobalPrefix.d.ts +25 -0
  311. package/dist/workerGlobalPrefix.d.ts.map +1 -0
  312. package/dist/workerGlobalPrefix.js +37 -5
  313. package/dist/workerGlobalPrefix.js.map +1 -0
  314. package/dist/workerTypes.d.ts +52 -0
  315. package/dist/workerTypes.d.ts.map +1 -0
  316. package/dist/workerTypes.js +24 -0
  317. package/dist/workerTypes.js.map +1 -0
  318. package/dist/workers.d.ts +69 -0
  319. package/dist/workers.d.ts.map +1 -0
  320. package/dist/workers.js +68 -4
  321. package/dist/workers.js.map +1 -0
  322. package/npm-shrinkwrap.json +2 -2
  323. package/package.json +2 -1
  324. package/scripts/data_model.mjs +2058 -0
@@ -0,0 +1,2058 @@
1
+ /**
2
+ * Data model script.
3
+ *
4
+ * This script will fetch from the connectedhomeip GitHub repository the data model files and convert them to JSON.
5
+ *
6
+ * It supports environment variables to customize the version and output paths.
7
+ *
8
+ * MATTER_DATA_MODEL_VERSION - The default version is 1.4.2.
9
+ */
10
+
11
+ const MATTER_DATA_MODEL_VERSION = process.env.MATTER_DATA_MODEL_VERSION || '1.4.2';
12
+ const SRC_PATH = `https://raw.githubusercontent.com/project-chip/connectedhomeip/master/data_model/${MATTER_DATA_MODEL_VERSION}/`;
13
+ const DATA_MODEL_PATHS = {
14
+ clusters: `${SRC_PATH}clusters/`,
15
+ clustersIds: `${SRC_PATH}clusters/cluster_ids.json`,
16
+ deviceTypes: `${SRC_PATH}device_types/`,
17
+ deviceTypesIds: `${SRC_PATH}device_types/device_type_ids.json`,
18
+ namespaces: `${SRC_PATH}namespaces/`,
19
+ };
20
+ const DST_PATH = `chip/${MATTER_DATA_MODEL_VERSION}/`;
21
+ const OUTPUT_NAMESPACES = 'namespaces.json';
22
+ const OUTPUT_DEVICE_TYPES = 'deviceTypes.json';
23
+ const OUTPUT_CLUSTERS = 'clusters.json';
24
+ const GITHUB_API_BASE = 'https://api.github.com/repos/project-chip/connectedhomeip/contents';
25
+ const DEVICE_TYPES_DIRECTORY_API = `${GITHUB_API_BASE}/data_model/${MATTER_DATA_MODEL_VERSION}/device_types?ref=master`;
26
+ const CLUSTERS_DIRECTORY_API = `${GITHUB_API_BASE}/data_model/${MATTER_DATA_MODEL_VERSION}/clusters?ref=master`;
27
+ const GITHUB_API_HEADERS = {
28
+ 'User-Agent': 'matterbridge-data-model-script',
29
+ Accept: 'application/vnd.github.v3+json',
30
+ };
31
+
32
+ const sanitizeKey = (value) => value.replace(/[\s/]+/g, '');
33
+ const normalizeDisplayName = (value) => (typeof value === 'string' ? value.replace(/\s*\/\s*/g, '') : value);
34
+
35
+ const cloneDeep = (value) => {
36
+ if (value === undefined) {
37
+ return undefined;
38
+ }
39
+
40
+ return typeof structuredClone === 'function' ? structuredClone(value) : JSON.parse(JSON.stringify(value));
41
+ };
42
+
43
+ import { mkdir, writeFile } from 'node:fs/promises';
44
+ import { join } from 'node:path';
45
+ import { request } from 'node:https';
46
+
47
+ const fetchRemoteText = async (url, { description, headers = {}, maxRedirects = 5 } = {}) => {
48
+ const label = description || url;
49
+
50
+ const download = (target, redirectCount = 0) =>
51
+ new Promise((resolve, reject) => {
52
+ const req = request(target, { headers }, (res) => {
53
+ const { statusCode = 0, headers: responseHeaders } = res;
54
+
55
+ if (statusCode >= 300 && statusCode < 400 && responseHeaders.location) {
56
+ if (redirectCount >= maxRedirects) {
57
+ reject(new Error(`Too many redirects while fetching ${label}`));
58
+ return;
59
+ }
60
+
61
+ const redirectUrl = new URL(responseHeaders.location, target).toString();
62
+ resolve(download(redirectUrl, redirectCount + 1));
63
+ return;
64
+ }
65
+
66
+ if (statusCode < 200 || statusCode >= 300) {
67
+ reject(new Error(`Failed to download ${label}: ${statusCode}`));
68
+ return;
69
+ }
70
+
71
+ const chunks = [];
72
+ res.on('data', (chunk) => chunks.push(chunk));
73
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
74
+ });
75
+
76
+ req.on('error', (error) => {
77
+ reject(new Error(`Request error while fetching ${label}: ${error.message}`));
78
+ });
79
+
80
+ req.end();
81
+ });
82
+
83
+ return download(url);
84
+ };
85
+
86
+ const fetchJson = async (url, { description, headers, maxRedirects } = {}) => {
87
+ const text = await fetchRemoteText(url, { description, headers, maxRedirects });
88
+
89
+ try {
90
+ return JSON.parse(text);
91
+ } catch (error) {
92
+ throw new Error(`Unable to parse JSON from ${description || url}: ${error.message}`);
93
+ }
94
+ };
95
+
96
+ /* eslint-disable no-console */
97
+
98
+ const decodeEntities = (value) =>
99
+ value
100
+ .replace(/&amp;/g, '&')
101
+ .replace(/&lt;/g, '<')
102
+ .replace(/&gt;/g, '>')
103
+ .replace(/&quot;/g, '"')
104
+ .replace(/&apos;/g, "'");
105
+
106
+ const parseHexId = (rawValue, context) => {
107
+ const value = rawValue.trim();
108
+ const match = value.match(/^0x([0-9a-fA-F]+)$/);
109
+
110
+ if (!match) {
111
+ throw new Error(`Invalid hex id "${rawValue}" encountered while parsing ${context}.`);
112
+ }
113
+
114
+ const digits = match[1];
115
+
116
+ return Number.parseInt(digits, 16);
117
+ };
118
+
119
+ const parseAttributes = (fragment) => {
120
+ const attributes = {};
121
+ const attrRegex = /([A-Za-z_:][A-Za-z0-9_.:-]*)\s*=\s*"([^"]*)"/g;
122
+
123
+ let match;
124
+ while ((match = attrRegex.exec(fragment)) !== null) {
125
+ const [, key, rawValue] = match;
126
+ attributes[key] = decodeEntities(rawValue.trim());
127
+ }
128
+
129
+ return attributes;
130
+ };
131
+
132
+ const parseNumericValue = (rawValue) => {
133
+ if (rawValue === undefined) {
134
+ return undefined;
135
+ }
136
+
137
+ const value = rawValue.trim();
138
+
139
+ if (/^[-+]?0x[0-9a-f]+$/i.test(value)) {
140
+ return Number.parseInt(value, 16);
141
+ }
142
+
143
+ if (/^[-+]?\d+$/.test(value)) {
144
+ const parsed = Number.parseInt(value, 10);
145
+ return Number.isNaN(parsed) ? value : parsed;
146
+ }
147
+
148
+ return value;
149
+ };
150
+
151
+ const parseScalarValue = (rawValue) => {
152
+ if (rawValue === undefined) {
153
+ return undefined;
154
+ }
155
+
156
+ const trimmed = rawValue.trim();
157
+ if (!trimmed) {
158
+ return '';
159
+ }
160
+
161
+ const normalized = trimmed.toLowerCase();
162
+ if (normalized === 'true') {
163
+ return true;
164
+ }
165
+
166
+ if (normalized === 'false') {
167
+ return false;
168
+ }
169
+
170
+ const numeric = parseNumericValue(trimmed);
171
+ if (typeof numeric === 'number') {
172
+ return numeric;
173
+ }
174
+
175
+ return trimmed;
176
+ };
177
+
178
+ const toCamelCase = (value) => value.replace(/[-_:](\w)/g, (match, letter) => letter.toUpperCase());
179
+
180
+ const prefixDirectiveKey = (prefix, key) => {
181
+ const camelKey = toCamelCase(key);
182
+ return `${prefix}${camelKey.charAt(0).toUpperCase()}${camelKey.slice(1)}`;
183
+ };
184
+
185
+ const CONFORMANCE_TAGS = [
186
+ ['mandatoryConform', 'mandatory'],
187
+ ['optionalConform', 'optional'],
188
+ ['disallowConform', 'disallow'],
189
+ ['provisionalConform', 'provisional'],
190
+ ['deprecateConform', 'deprecate'],
191
+ ['otherwiseConform', 'otherwise'],
192
+ ];
193
+
194
+ const parseConformanceEntries = (fragment) => {
195
+ if (!fragment) {
196
+ return [];
197
+ }
198
+
199
+ const entries = [];
200
+
201
+ for (const [tag, status] of CONFORMANCE_TAGS) {
202
+ const regex = new RegExp(`<${tag}\\b[^>]*>`, 'gi');
203
+ let match;
204
+
205
+ while ((match = regex.exec(fragment)) !== null) {
206
+ const attributes = parseAttributes(match[0]);
207
+ const entry = { status };
208
+
209
+ if (Object.keys(attributes).length > 0) {
210
+ entry.attributes = attributes;
211
+ }
212
+
213
+ entries.push(entry);
214
+ }
215
+ }
216
+
217
+ return entries;
218
+ };
219
+
220
+ const extractConditions = (fragment) => {
221
+ if (!fragment) {
222
+ return [];
223
+ }
224
+
225
+ const conditions = [];
226
+ const regex = /<condition\b[^>]*>/gi;
227
+ let match;
228
+
229
+ while ((match = regex.exec(fragment)) !== null) {
230
+ const attributes = parseAttributes(match[0]);
231
+ const { name, summary = '', ...rest } = attributes;
232
+
233
+ if (!name) {
234
+ continue;
235
+ }
236
+
237
+ const condition = { name };
238
+
239
+ if (summary) {
240
+ condition.summary = summary;
241
+ }
242
+
243
+ if (Object.keys(rest).length > 0) {
244
+ condition.attributes = rest;
245
+ }
246
+
247
+ conditions.push(condition);
248
+ }
249
+
250
+ return conditions;
251
+ };
252
+
253
+ const extractFeatureRefs = (fragment) => {
254
+ if (!fragment) {
255
+ return [];
256
+ }
257
+
258
+ const names = new Set();
259
+ const regex = /<feature\b[^>]*>/gi;
260
+ let match;
261
+
262
+ while ((match = regex.exec(fragment)) !== null) {
263
+ const attributes = parseAttributes(match[0]);
264
+ const { name } = attributes;
265
+
266
+ if (name) {
267
+ names.add(name);
268
+ }
269
+ }
270
+
271
+ return [...names];
272
+ };
273
+
274
+ const extractDirectiveList = (fragment, tagName) => {
275
+ if (!fragment) {
276
+ return [];
277
+ }
278
+
279
+ const regex = new RegExp(`<${tagName}\\b[^>]*>`, 'gi');
280
+ const directives = [];
281
+ let match;
282
+
283
+ while ((match = regex.exec(fragment)) !== null) {
284
+ const attributes = parseAttributes(match[0]);
285
+
286
+ if (Object.keys(attributes).length > 0) {
287
+ directives.push(attributes);
288
+ } else {
289
+ directives.push({});
290
+ }
291
+ }
292
+
293
+ return directives;
294
+ };
295
+
296
+ const extractAccessDirectives = (fragment) => extractDirectiveList(fragment, 'access');
297
+ const extractQualityDirectives = (fragment) => extractDirectiveList(fragment, 'quality');
298
+
299
+ const extractEntryDirectives = (fragment) => {
300
+ if (!fragment) {
301
+ return [];
302
+ }
303
+
304
+ const regex = /<entry\b[^>]*>/gi;
305
+ const entries = [];
306
+ let match;
307
+
308
+ while ((match = regex.exec(fragment)) !== null) {
309
+ const attributes = parseAttributes(match[0]);
310
+
311
+ if (Object.keys(attributes).length > 0) {
312
+ entries.push(attributes);
313
+ } else {
314
+ entries.push({});
315
+ }
316
+ }
317
+
318
+ return entries;
319
+ };
320
+
321
+ const extractConstraintSnippets = (fragment) => {
322
+ if (!fragment) {
323
+ return [];
324
+ }
325
+
326
+ const regex = /<constraint\b[^>]*>([\s\S]*?)<\/constraint>/gi;
327
+ const constraints = [];
328
+ let match;
329
+
330
+ while ((match = regex.exec(fragment)) !== null) {
331
+ const [, body] = match;
332
+ const normalized = body.replace(/\s+/g, ' ').trim();
333
+
334
+ if (normalized) {
335
+ constraints.push(normalized);
336
+ }
337
+ }
338
+
339
+ return constraints;
340
+ };
341
+
342
+ const removeSegments = (source, segments) => {
343
+ if (!segments || segments.length === 0) {
344
+ return source;
345
+ }
346
+
347
+ const sorted = [...segments].sort((left, right) => left.start - right.start);
348
+ let result = '';
349
+ let cursor = 0;
350
+
351
+ for (const { start, end } of sorted) {
352
+ if (cursor < start) {
353
+ result += source.slice(cursor, start);
354
+ }
355
+
356
+ cursor = Math.max(cursor, end);
357
+ }
358
+
359
+ if (cursor < source.length) {
360
+ result += source.slice(cursor);
361
+ }
362
+
363
+ return result;
364
+ };
365
+
366
+ const parseEnumItems = (enumBody, enumName, clusterName, contextLabel) => {
367
+ const items = [];
368
+ const itemRegex = /<item\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/item>)/gi;
369
+ let match;
370
+
371
+ while ((match = itemRegex.exec(enumBody)) !== null) {
372
+ const fragment = match[0];
373
+ const openingMatch = fragment.match(/<item\b[^>]*>/i);
374
+
375
+ if (!openingMatch) {
376
+ throw new Error(`Unable to parse enum item in ${enumName} for cluster ${clusterName} (${contextLabel}).`);
377
+ }
378
+
379
+ const attributes = parseAttributes(openingMatch[0]);
380
+ const { name, summary = '', value, ...rest } = attributes;
381
+
382
+ if (!name) {
383
+ throw new Error(`Enum item without a name encountered in ${enumName} for cluster ${clusterName} (${contextLabel}).`);
384
+ }
385
+
386
+ const entry = { name };
387
+
388
+ if (value !== undefined) {
389
+ const parsedValue = parseNumericValue(value);
390
+ entry.value = parsedValue !== undefined ? parsedValue : value;
391
+ }
392
+
393
+ if (summary) {
394
+ entry.summary = summary;
395
+ }
396
+
397
+ if (Object.keys(rest).length > 0) {
398
+ entry.attributes = rest;
399
+ }
400
+
401
+ let body = '';
402
+
403
+ if (!fragment.trimEnd().endsWith('/>')) {
404
+ body = fragment.slice(openingMatch[0].length, fragment.length - '</item>'.length);
405
+ }
406
+
407
+ const conformances = parseConformanceEntries(body);
408
+ if (conformances.length > 0) {
409
+ entry.conformance = conformances;
410
+ }
411
+
412
+ const conditions = extractConditions(body);
413
+ if (conditions.length > 0) {
414
+ entry.conditions = conditions;
415
+ }
416
+
417
+ const features = extractFeatureRefs(body);
418
+ if (features.length > 0) {
419
+ entry.features = features;
420
+ }
421
+
422
+ const access = extractAccessDirectives(body);
423
+ if (access.length > 0) {
424
+ entry.access = access;
425
+ }
426
+
427
+ const quality = extractQualityDirectives(body);
428
+ if (quality.length > 0) {
429
+ entry.quality = quality;
430
+ }
431
+
432
+ const constraints = extractConstraintSnippets(body);
433
+ if (constraints.length > 0) {
434
+ entry.constraints = constraints;
435
+ }
436
+
437
+ items.push(entry);
438
+ }
439
+
440
+ return items;
441
+ };
442
+
443
+ const parseBitmapFields = (bitmapBody, bitmapName, clusterName, contextLabel) => {
444
+ const fields = [];
445
+ const fieldRegex = /<bitfield\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/bitfield>)/gi;
446
+ let match;
447
+
448
+ while ((match = fieldRegex.exec(bitmapBody)) !== null) {
449
+ const fragment = match[0];
450
+ const openingMatch = fragment.match(/<bitfield\b[^>]*>/i);
451
+
452
+ if (!openingMatch) {
453
+ throw new Error(`Unable to parse bitfield in ${bitmapName} for cluster ${clusterName} (${contextLabel}).`);
454
+ }
455
+
456
+ const attributes = parseAttributes(openingMatch[0]);
457
+ const { name, summary = '', bit, ...rest } = attributes;
458
+
459
+ if (!name) {
460
+ throw new Error(`Bitfield without a name encountered in ${bitmapName} for cluster ${clusterName} (${contextLabel}).`);
461
+ }
462
+
463
+ const entry = { name };
464
+
465
+ if (bit !== undefined) {
466
+ const parsedBit = parseNumericValue(bit);
467
+ entry.bit = parsedBit !== undefined ? parsedBit : bit;
468
+ }
469
+
470
+ if (summary) {
471
+ entry.summary = summary;
472
+ }
473
+
474
+ if (Object.keys(rest).length > 0) {
475
+ entry.attributes = rest;
476
+ }
477
+
478
+ let body = '';
479
+
480
+ if (!fragment.trimEnd().endsWith('/>')) {
481
+ body = fragment.slice(openingMatch[0].length, fragment.length - '</bitfield>'.length);
482
+ }
483
+
484
+ const conformances = parseConformanceEntries(body);
485
+ if (conformances.length > 0) {
486
+ entry.conformance = conformances;
487
+ }
488
+
489
+ const conditions = extractConditions(body);
490
+ if (conditions.length > 0) {
491
+ entry.conditions = conditions;
492
+ }
493
+
494
+ const features = extractFeatureRefs(body);
495
+ if (features.length > 0) {
496
+ entry.features = features;
497
+ }
498
+
499
+ const access = extractAccessDirectives(body);
500
+ if (access.length > 0) {
501
+ entry.access = access;
502
+ }
503
+
504
+ const quality = extractQualityDirectives(body);
505
+ if (quality.length > 0) {
506
+ entry.quality = quality;
507
+ }
508
+
509
+ const constraints = extractConstraintSnippets(body);
510
+ if (constraints.length > 0) {
511
+ entry.constraints = constraints;
512
+ }
513
+
514
+ fields.push(entry);
515
+ }
516
+
517
+ return fields;
518
+ };
519
+
520
+ const parseStructFields = (structBody, structName, clusterName, contextLabel) => {
521
+ const fields = [];
522
+ const segments = [];
523
+ const fieldRegex = /<field\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/field>)/gi;
524
+ let match;
525
+
526
+ while ((match = fieldRegex.exec(structBody)) !== null) {
527
+ const fragment = match[0];
528
+ const start = match.index;
529
+ const end = fieldRegex.lastIndex;
530
+ segments.push({ start, end });
531
+
532
+ const openingMatch = fragment.match(/<field\b[^>]*>/i);
533
+
534
+ if (!openingMatch) {
535
+ throw new Error(`Unable to parse struct field in ${structName} for cluster ${clusterName} (${contextLabel}).`);
536
+ }
537
+
538
+ const attributes = parseAttributes(openingMatch[0]);
539
+ const { name, summary = '', id, type, ...rest } = attributes;
540
+
541
+ if (!name) {
542
+ throw new Error(`Struct field without a name encountered in ${structName} for cluster ${clusterName} (${contextLabel}).`);
543
+ }
544
+
545
+ const fieldEntry = { name };
546
+
547
+ if (type !== undefined) {
548
+ fieldEntry.type = type;
549
+ }
550
+
551
+ if (id !== undefined) {
552
+ const parsedId = parseNumericValue(id);
553
+ fieldEntry.id = parsedId !== undefined ? parsedId : id;
554
+ }
555
+
556
+ if (summary) {
557
+ fieldEntry.summary = summary;
558
+ }
559
+
560
+ if (Object.keys(rest).length > 0) {
561
+ fieldEntry.attributes = rest;
562
+ }
563
+
564
+ let body = '';
565
+
566
+ if (!fragment.trimEnd().endsWith('/>')) {
567
+ body = fragment.slice(openingMatch[0].length, fragment.length - '</field>'.length);
568
+ }
569
+
570
+ const conformances = parseConformanceEntries(body);
571
+ if (conformances.length > 0) {
572
+ fieldEntry.conformance = conformances;
573
+ }
574
+
575
+ const conditions = extractConditions(body);
576
+ if (conditions.length > 0) {
577
+ fieldEntry.conditions = conditions;
578
+ }
579
+
580
+ const features = extractFeatureRefs(body);
581
+ if (features.length > 0) {
582
+ fieldEntry.features = features;
583
+ }
584
+
585
+ const access = extractAccessDirectives(body);
586
+ if (access.length > 0) {
587
+ fieldEntry.access = access;
588
+ }
589
+
590
+ const quality = extractQualityDirectives(body);
591
+ if (quality.length > 0) {
592
+ fieldEntry.quality = quality;
593
+ }
594
+
595
+ const entries = extractEntryDirectives(body);
596
+ if (entries.length > 0) {
597
+ fieldEntry.entries = entries;
598
+ }
599
+
600
+ const constraints = extractConstraintSnippets(body);
601
+ if (constraints.length > 0) {
602
+ fieldEntry.constraints = constraints;
603
+ }
604
+
605
+ fields.push(fieldEntry);
606
+ }
607
+
608
+ const remainder = removeSegments(structBody, segments);
609
+
610
+ return { fields, remainder };
611
+ };
612
+
613
+ const parseDataTypesBlock = (xmlContent, clusterName, contextLabel) => {
614
+ const match = xmlContent.match(/<dataTypes\b[^>]*>([\s\S]*?)<\/dataTypes>/i);
615
+
616
+ if (!match) {
617
+ return {};
618
+ }
619
+
620
+ const [, body] = match;
621
+ const dataTypes = {};
622
+ const regex = /<(enum|bitmap|struct)\b[^>]*>[\s\S]*?<\/\1>/gi;
623
+ let typeMatch;
624
+
625
+ while ((typeMatch = regex.exec(body)) !== null) {
626
+ const fullMatch = typeMatch[0];
627
+ const rawType = typeMatch[1];
628
+ const openingMatch = fullMatch.match(new RegExp(`<${rawType}\\b[^>]*>`, 'i'));
629
+
630
+ if (!openingMatch) {
631
+ throw new Error(`Unable to parse ${rawType} definition for cluster ${clusterName} (${contextLabel}).`);
632
+ }
633
+
634
+ const typeAttributes = parseAttributes(openingMatch[0]);
635
+ const { name, summary = '', ...rest } = typeAttributes;
636
+
637
+ if (!name) {
638
+ throw new Error(`Data type (${rawType}) without a name encountered in cluster ${clusterName} (${contextLabel}).`);
639
+ }
640
+
641
+ const typeBody = fullMatch.slice(openingMatch[0].length, fullMatch.length - `</${rawType}>`.length);
642
+
643
+ const definition = {
644
+ type: rawType.toLowerCase(),
645
+ name,
646
+ };
647
+
648
+ if (summary) {
649
+ definition.summary = summary;
650
+ }
651
+
652
+ if (Object.keys(rest).length > 0) {
653
+ definition.attributes = rest;
654
+ }
655
+
656
+ if (definition.type === 'enum') {
657
+ const entries = parseEnumItems(typeBody, name, clusterName, contextLabel);
658
+ if (entries.length > 0) {
659
+ definition.entries = entries;
660
+ }
661
+ } else if (definition.type === 'bitmap') {
662
+ const fields = parseBitmapFields(typeBody, name, clusterName, contextLabel);
663
+ if (fields.length > 0) {
664
+ definition.fields = fields;
665
+ }
666
+ } else if (definition.type === 'struct') {
667
+ const { fields, remainder } = parseStructFields(typeBody, name, clusterName, contextLabel);
668
+
669
+ if (fields.length > 0) {
670
+ definition.fields = fields;
671
+ }
672
+
673
+ const structConformance = parseConformanceEntries(remainder);
674
+ if (structConformance.length > 0) {
675
+ definition.conformance = structConformance;
676
+ }
677
+
678
+ const structConditions = extractConditions(remainder);
679
+ if (structConditions.length > 0) {
680
+ definition.conditions = structConditions;
681
+ }
682
+
683
+ const structFeatures = extractFeatureRefs(remainder);
684
+ if (structFeatures.length > 0) {
685
+ definition.features = structFeatures;
686
+ }
687
+
688
+ const structAccess = extractAccessDirectives(remainder);
689
+ if (structAccess.length > 0) {
690
+ definition.access = structAccess;
691
+ }
692
+
693
+ const structQuality = extractQualityDirectives(remainder);
694
+ if (structQuality.length > 0) {
695
+ definition.quality = structQuality;
696
+ }
697
+
698
+ const structEntries = extractEntryDirectives(remainder);
699
+ if (structEntries.length > 0) {
700
+ definition.entries = structEntries;
701
+ }
702
+
703
+ const structConstraints = extractConstraintSnippets(remainder);
704
+ if (structConstraints.length > 0) {
705
+ definition.constraints = structConstraints;
706
+ }
707
+ }
708
+
709
+ dataTypes[name] = definition;
710
+ }
711
+
712
+ return dataTypes;
713
+ };
714
+
715
+ const parseAttributesBlock = (xmlContent, clusterName, contextLabel) => {
716
+ const match = xmlContent.match(/<attributes\b[^>]*>([\s\S]*?)<\/attributes>/i);
717
+
718
+ if (!match) {
719
+ return {};
720
+ }
721
+
722
+ const [, body] = match;
723
+ const attributes = {};
724
+ const attributeRegex = /<attribute\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/attribute>)/gi;
725
+ let attrMatch;
726
+
727
+ while ((attrMatch = attributeRegex.exec(body)) !== null) {
728
+ const fullMatch = attrMatch[0];
729
+ const openingMatch = fullMatch.match(/<attribute\b[^>]*>/i);
730
+
731
+ if (!openingMatch) {
732
+ throw new Error(`Unable to parse attribute definition for cluster ${clusterName} (${contextLabel}).`);
733
+ }
734
+
735
+ const attributeAttributes = parseAttributes(openingMatch[0]);
736
+ const { id, name, type, summary = '', ...rest } = attributeAttributes;
737
+
738
+ if (!name) {
739
+ throw new Error(`Attribute without a name encountered in cluster ${clusterName} (${contextLabel}).`);
740
+ }
741
+
742
+ const entry = { name };
743
+
744
+ if (id !== undefined) {
745
+ entry.id = parseHexId(id, `attribute ${name} in cluster ${clusterName}`);
746
+ }
747
+
748
+ if (type !== undefined) {
749
+ entry.type = type;
750
+ }
751
+
752
+ if (summary) {
753
+ entry.summary = summary;
754
+ }
755
+
756
+ for (const [key, value] of Object.entries(rest)) {
757
+ const normalizedKey = toCamelCase(key);
758
+ entry[normalizedKey] = parseScalarValue(value);
759
+ }
760
+
761
+ let attributeBody = '';
762
+
763
+ if (!fullMatch.trimEnd().endsWith('/>')) {
764
+ attributeBody = fullMatch.slice(openingMatch[0].length, fullMatch.length - '</attribute>'.length);
765
+ }
766
+
767
+ const directiveSegments = [];
768
+
769
+ const accessRegex = /<access\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/access>)/gi;
770
+ let accessMatch;
771
+ while ((accessMatch = accessRegex.exec(attributeBody)) !== null) {
772
+ directiveSegments.push({ start: accessMatch.index, end: accessRegex.lastIndex });
773
+ const accessAttributes = parseAttributes(accessMatch[0]);
774
+
775
+ for (const [key, value] of Object.entries(accessAttributes)) {
776
+ const propertyKey = prefixDirectiveKey('access', key);
777
+ entry[propertyKey] = parseScalarValue(value);
778
+ }
779
+ }
780
+
781
+ const qualityRegex = /<quality\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/quality>)/gi;
782
+ let qualityMatch;
783
+ while ((qualityMatch = qualityRegex.exec(attributeBody)) !== null) {
784
+ directiveSegments.push({ start: qualityMatch.index, end: qualityRegex.lastIndex });
785
+ const qualityAttributes = parseAttributes(qualityMatch[0]);
786
+
787
+ for (const [key, value] of Object.entries(qualityAttributes)) {
788
+ const propertyKey = prefixDirectiveKey('quality', key);
789
+ entry[propertyKey] = parseScalarValue(value);
790
+ }
791
+ }
792
+
793
+ const remainder = removeSegments(attributeBody, directiveSegments);
794
+
795
+ const conformances = parseConformanceEntries(remainder);
796
+ if (conformances.length > 0) {
797
+ entry.conformance = conformances;
798
+ }
799
+
800
+ const conditions = extractConditions(remainder);
801
+ if (conditions.length > 0) {
802
+ entry.conditions = conditions;
803
+ }
804
+
805
+ const features = extractFeatureRefs(remainder);
806
+ if (features.length > 0) {
807
+ entry.features = features;
808
+ }
809
+
810
+ const entries = extractEntryDirectives(remainder);
811
+ if (entries.length > 0) {
812
+ entry.entries = entries;
813
+ }
814
+
815
+ const constraints = extractConstraintSnippets(remainder);
816
+ if (constraints.length > 0) {
817
+ entry.constraints = constraints;
818
+ }
819
+
820
+ attributes[name] = entry;
821
+ }
822
+
823
+ return attributes;
824
+ };
825
+
826
+ const parseCommandArguments = (commandBody, commandName, clusterName, contextLabel, directiveSegments) => {
827
+ const argumentsMap = {};
828
+
829
+ const processTag = (tagName) => {
830
+ const regex = new RegExp(`<${tagName}\\b[^>]*?(?:\\/\\s*>|>[\\s\\S]*?<\\/${tagName}>)`, 'gi');
831
+ regex.lastIndex = 0;
832
+ let match;
833
+
834
+ while ((match = regex.exec(commandBody)) !== null) {
835
+ directiveSegments.push({ start: match.index, end: regex.lastIndex });
836
+ const fragment = match[0];
837
+ const openingMatch = fragment.match(new RegExp(`<${tagName}\\b[^>]*>`, 'i'));
838
+
839
+ if (!openingMatch) {
840
+ throw new Error(`Unable to parse ${tagName} definition for command ${commandName} in cluster ${clusterName} (${contextLabel}).`);
841
+ }
842
+
843
+ const parameterAttributes = parseAttributes(openingMatch[0]);
844
+ const { name, id, fieldId, type, summary = '', ...rest } = parameterAttributes;
845
+
846
+ if (!name) {
847
+ throw new Error(`Parameter without a name encountered in command ${commandName} for cluster ${clusterName} (${contextLabel}).`);
848
+ }
849
+
850
+ const parameterEntry = { name };
851
+
852
+ if (id !== undefined) {
853
+ parameterEntry.id = parseNumericValue(id);
854
+ }
855
+
856
+ if (fieldId !== undefined) {
857
+ parameterEntry.fieldId = parseNumericValue(fieldId);
858
+ }
859
+
860
+ if (type !== undefined) {
861
+ parameterEntry.type = type;
862
+ }
863
+
864
+ if (summary) {
865
+ parameterEntry.summary = summary;
866
+ }
867
+
868
+ for (const [key, value] of Object.entries(rest)) {
869
+ const normalizedKey = toCamelCase(key);
870
+ parameterEntry[normalizedKey] = parseScalarValue(value);
871
+ }
872
+
873
+ let parameterBody = '';
874
+
875
+ if (!fragment.trimEnd().endsWith('/>')) {
876
+ parameterBody = fragment.slice(openingMatch[0].length, fragment.length - `</${tagName}>`.length);
877
+ }
878
+
879
+ const parameterConformance = parseConformanceEntries(parameterBody);
880
+ if (parameterConformance.length > 0) {
881
+ parameterEntry.conformance = parameterConformance;
882
+ }
883
+
884
+ const parameterConditions = extractConditions(parameterBody);
885
+ if (parameterConditions.length > 0) {
886
+ parameterEntry.conditions = parameterConditions;
887
+ }
888
+
889
+ const parameterFeatures = extractFeatureRefs(parameterBody);
890
+ if (parameterFeatures.length > 0) {
891
+ parameterEntry.features = parameterFeatures;
892
+ }
893
+
894
+ const parameterConstraints = extractConstraintSnippets(parameterBody);
895
+ if (parameterConstraints.length > 0) {
896
+ parameterEntry.constraints = parameterConstraints;
897
+ }
898
+
899
+ if (argumentsMap[name]) {
900
+ console.log(`Warning: Duplicate parameter name "${name}" encountered in command ${commandName} for cluster ${clusterName} (${contextLabel}). Overwriting previous entry.`);
901
+ }
902
+
903
+ argumentsMap[name] = parameterEntry;
904
+ }
905
+ };
906
+
907
+ processTag('arg');
908
+ processTag('field');
909
+
910
+ return argumentsMap;
911
+ };
912
+
913
+ const parseCommandsBlock = (xmlContent, clusterName, contextLabel) => {
914
+ const commandRegex = /<command\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/command>)/gi;
915
+ if (!commandRegex.test(xmlContent)) {
916
+ return {};
917
+ }
918
+
919
+ commandRegex.lastIndex = 0;
920
+ const commands = {};
921
+ let commandMatch;
922
+
923
+ while ((commandMatch = commandRegex.exec(xmlContent)) !== null) {
924
+ const fragment = commandMatch[0];
925
+ const openingMatch = fragment.match(/<command\b[^>]*>/i);
926
+
927
+ if (!openingMatch) {
928
+ throw new Error(`Unable to parse command definition for cluster ${clusterName} (${contextLabel}).`);
929
+ }
930
+
931
+ const commandAttributes = parseAttributes(openingMatch[0]);
932
+ const { name, code, summary = '', ...rest } = commandAttributes;
933
+
934
+ if (!name) {
935
+ throw new Error(`Command without a name encountered in cluster ${clusterName} (${contextLabel}).`);
936
+ }
937
+
938
+ const commandEntry = { name };
939
+
940
+ if (code !== undefined) {
941
+ commandEntry.id = parseNumericValue(code);
942
+ }
943
+
944
+ if (summary) {
945
+ commandEntry.summary = summary;
946
+ }
947
+
948
+ for (const [key, value] of Object.entries(rest)) {
949
+ const normalizedKey = toCamelCase(key);
950
+ commandEntry[normalizedKey] = parseScalarValue(value);
951
+ }
952
+
953
+ let commandBody = '';
954
+
955
+ if (!fragment.trimEnd().endsWith('/>')) {
956
+ commandBody = fragment.slice(openingMatch[0].length, fragment.length - '</command>'.length);
957
+ }
958
+
959
+ const directiveSegments = [];
960
+
961
+ const descriptionMatch = commandBody.match(/<description>([\s\S]*?)<\/description>/i);
962
+ if (descriptionMatch) {
963
+ const [matchText, body] = descriptionMatch;
964
+ const start = descriptionMatch.index ?? commandBody.indexOf(matchText);
965
+ const end = start + matchText.length;
966
+ directiveSegments.push({ start, end });
967
+ const description = body.replace(/\s+/g, ' ').trim();
968
+ if (description) {
969
+ commandEntry.description = description;
970
+ }
971
+ }
972
+
973
+ const argumentsMap = parseCommandArguments(commandBody, name, clusterName, contextLabel, directiveSegments);
974
+ if (Object.keys(argumentsMap).length > 0) {
975
+ commandEntry.arguments = argumentsMap;
976
+ }
977
+
978
+ const accessRegex = /<access\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/access>)/gi;
979
+ let accessMatch;
980
+ while ((accessMatch = accessRegex.exec(commandBody)) !== null) {
981
+ directiveSegments.push({ start: accessMatch.index, end: accessRegex.lastIndex });
982
+ const accessAttributes = parseAttributes(accessMatch[0]);
983
+
984
+ for (const [key, value] of Object.entries(accessAttributes)) {
985
+ const propertyKey = prefixDirectiveKey('access', key);
986
+ commandEntry[propertyKey] = parseScalarValue(value);
987
+ }
988
+ }
989
+
990
+ const qualityRegex = /<quality\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/quality>)/gi;
991
+ let qualityMatch;
992
+ while ((qualityMatch = qualityRegex.exec(commandBody)) !== null) {
993
+ directiveSegments.push({ start: qualityMatch.index, end: qualityRegex.lastIndex });
994
+ const qualityAttributes = parseAttributes(qualityMatch[0]);
995
+
996
+ for (const [key, value] of Object.entries(qualityAttributes)) {
997
+ const propertyKey = prefixDirectiveKey('quality', key);
998
+ commandEntry[propertyKey] = parseScalarValue(value);
999
+ }
1000
+ }
1001
+
1002
+ const remainder = removeSegments(commandBody, directiveSegments);
1003
+
1004
+ const conformances = parseConformanceEntries(remainder);
1005
+ if (conformances.length > 0) {
1006
+ commandEntry.conformance = conformances;
1007
+ }
1008
+
1009
+ const conditions = extractConditions(remainder);
1010
+ if (conditions.length > 0) {
1011
+ commandEntry.conditions = conditions;
1012
+ }
1013
+
1014
+ const features = extractFeatureRefs(remainder);
1015
+ if (features.length > 0) {
1016
+ commandEntry.features = features;
1017
+ }
1018
+
1019
+ const constraints = extractConstraintSnippets(remainder);
1020
+ if (constraints.length > 0) {
1021
+ commandEntry.constraints = constraints;
1022
+ }
1023
+
1024
+ if (commands[name]) {
1025
+ console.log(`Warning: Duplicate command name "${name}" encountered in cluster ${clusterName} (${contextLabel}). Overwriting previous entry.`);
1026
+ }
1027
+
1028
+ commands[name] = commandEntry;
1029
+ }
1030
+
1031
+ return commands;
1032
+ };
1033
+
1034
+ const parseEventsBlock = (xmlContent, clusterName, contextLabel) => {
1035
+ const eventRegex = /<event\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/event>)/gi;
1036
+ if (!eventRegex.test(xmlContent)) {
1037
+ return {};
1038
+ }
1039
+
1040
+ eventRegex.lastIndex = 0;
1041
+ const events = {};
1042
+ let eventMatch;
1043
+
1044
+ while ((eventMatch = eventRegex.exec(xmlContent)) !== null) {
1045
+ const fragment = eventMatch[0];
1046
+ const openingMatch = fragment.match(/<event\b[^>]*>/i);
1047
+
1048
+ if (!openingMatch) {
1049
+ throw new Error(`Unable to parse event definition for cluster ${clusterName} (${contextLabel}).`);
1050
+ }
1051
+
1052
+ const eventAttributes = parseAttributes(openingMatch[0]);
1053
+ const { name, code, summary = '', ...rest } = eventAttributes;
1054
+
1055
+ if (!name) {
1056
+ throw new Error(`Event without a name encountered in cluster ${clusterName} (${contextLabel}).`);
1057
+ }
1058
+
1059
+ const eventEntry = { name };
1060
+
1061
+ if (code !== undefined) {
1062
+ eventEntry.id = parseNumericValue(code);
1063
+ }
1064
+
1065
+ if (summary) {
1066
+ eventEntry.summary = summary;
1067
+ }
1068
+
1069
+ for (const [key, value] of Object.entries(rest)) {
1070
+ const normalizedKey = toCamelCase(key);
1071
+ eventEntry[normalizedKey] = parseScalarValue(value);
1072
+ }
1073
+
1074
+ let eventBody = '';
1075
+
1076
+ if (!fragment.trimEnd().endsWith('/>')) {
1077
+ eventBody = fragment.slice(openingMatch[0].length, fragment.length - '</event>'.length);
1078
+ }
1079
+
1080
+ const directiveSegments = [];
1081
+
1082
+ const descriptionMatch = eventBody.match(/<description>([\s\S]*?)<\/description>/i);
1083
+ if (descriptionMatch) {
1084
+ const [matchText, body] = descriptionMatch;
1085
+ const start = descriptionMatch.index ?? eventBody.indexOf(matchText);
1086
+ const end = start + matchText.length;
1087
+ directiveSegments.push({ start, end });
1088
+ const description = body.replace(/\s+/g, ' ').trim();
1089
+ if (description) {
1090
+ eventEntry.description = description;
1091
+ }
1092
+ }
1093
+
1094
+ const fieldRegex = /<field\b[^>]*?(?:\/\s*>|>[\s\S]*?<\/field>)/gi;
1095
+ const fields = {};
1096
+ let fieldMatch;
1097
+
1098
+ while ((fieldMatch = fieldRegex.exec(eventBody)) !== null) {
1099
+ directiveSegments.push({ start: fieldMatch.index, end: fieldRegex.lastIndex });
1100
+ const fieldFragment = fieldMatch[0];
1101
+ const fieldOpeningMatch = fieldFragment.match(/<field\b[^>]*>/i);
1102
+
1103
+ if (!fieldOpeningMatch) {
1104
+ throw new Error(`Unable to parse field definition for event ${name} in cluster ${clusterName} (${contextLabel}).`);
1105
+ }
1106
+
1107
+ const fieldAttributes = parseAttributes(fieldOpeningMatch[0]);
1108
+ const { name: fieldName, id, type, summary: fieldSummary = '', ...fieldRest } = fieldAttributes;
1109
+
1110
+ if (!fieldName) {
1111
+ throw new Error(`Field without a name encountered in event ${name} for cluster ${clusterName} (${contextLabel}).`);
1112
+ }
1113
+
1114
+ const fieldEntry = { name: fieldName };
1115
+
1116
+ if (id !== undefined) {
1117
+ fieldEntry.id = parseNumericValue(id);
1118
+ }
1119
+
1120
+ if (type !== undefined) {
1121
+ fieldEntry.type = type;
1122
+ }
1123
+
1124
+ if (fieldSummary) {
1125
+ fieldEntry.summary = fieldSummary;
1126
+ }
1127
+
1128
+ for (const [key, value] of Object.entries(fieldRest)) {
1129
+ const normalizedKey = toCamelCase(key);
1130
+ fieldEntry[normalizedKey] = parseScalarValue(value);
1131
+ }
1132
+
1133
+ let fieldBody = '';
1134
+
1135
+ if (!fieldFragment.trimEnd().endsWith('/>')) {
1136
+ fieldBody = fieldFragment.slice(fieldOpeningMatch[0].length, fieldFragment.length - '</field>'.length);
1137
+ }
1138
+
1139
+ const fieldConformance = parseConformanceEntries(fieldBody);
1140
+ if (fieldConformance.length > 0) {
1141
+ fieldEntry.conformance = fieldConformance;
1142
+ }
1143
+
1144
+ const fieldConditions = extractConditions(fieldBody);
1145
+ if (fieldConditions.length > 0) {
1146
+ fieldEntry.conditions = fieldConditions;
1147
+ }
1148
+
1149
+ const fieldFeatures = extractFeatureRefs(fieldBody);
1150
+ if (fieldFeatures.length > 0) {
1151
+ fieldEntry.features = fieldFeatures;
1152
+ }
1153
+
1154
+ const fieldConstraints = extractConstraintSnippets(fieldBody);
1155
+ if (fieldConstraints.length > 0) {
1156
+ fieldEntry.constraints = fieldConstraints;
1157
+ }
1158
+
1159
+ fields[fieldName] = fieldEntry;
1160
+ }
1161
+
1162
+ if (Object.keys(fields).length > 0) {
1163
+ eventEntry.fields = fields;
1164
+ }
1165
+
1166
+ const remainder = removeSegments(eventBody, directiveSegments);
1167
+
1168
+ const conformances = parseConformanceEntries(remainder);
1169
+ if (conformances.length > 0) {
1170
+ eventEntry.conformance = conformances;
1171
+ }
1172
+
1173
+ const conditions = extractConditions(remainder);
1174
+ if (conditions.length > 0) {
1175
+ eventEntry.conditions = conditions;
1176
+ }
1177
+
1178
+ const features = extractFeatureRefs(remainder);
1179
+ if (features.length > 0) {
1180
+ eventEntry.features = features;
1181
+ }
1182
+
1183
+ const constraints = extractConstraintSnippets(remainder);
1184
+ if (constraints.length > 0) {
1185
+ eventEntry.constraints = constraints;
1186
+ }
1187
+
1188
+ if (events[name]) {
1189
+ console.log(`Warning: Duplicate event name "${name}" encountered in cluster ${clusterName} (${contextLabel}). Overwriting previous entry.`);
1190
+ }
1191
+
1192
+ events[name] = eventEntry;
1193
+ }
1194
+
1195
+ return events;
1196
+ };
1197
+
1198
+ const parseNamespaceXml = (xmlContent) => {
1199
+ const namespaceTagMatch = xmlContent.match(/<namespace[\s\S]*?>/i);
1200
+ if (!namespaceTagMatch) {
1201
+ throw new Error('Unable to locate namespace definition.');
1202
+ }
1203
+
1204
+ const namespaceAttributes = parseAttributes(namespaceTagMatch[0]);
1205
+ const { id, name } = namespaceAttributes;
1206
+
1207
+ if (!id || !name) {
1208
+ throw new Error('Namespace attributes "id" or "name" are missing.');
1209
+ }
1210
+
1211
+ const tagMatches = [...xmlContent.matchAll(/<tag\b[^>]*>/gi)];
1212
+ const tags = tagMatches.map((match) => {
1213
+ const tagAttributes = parseAttributes(match[0]);
1214
+ const { id: tagId, name: tagName } = tagAttributes;
1215
+
1216
+ if (!tagId || !tagName) {
1217
+ throw new Error('Tag attributes "id" or "name" are missing.');
1218
+ }
1219
+
1220
+ return { id: tagId, name: tagName };
1221
+ });
1222
+
1223
+ const hexId = parseHexId(id, `namespace ${name || 'unknown'}`);
1224
+ const hexTags = tags.map(({ id: tagId, name: tagName }) => ({
1225
+ id: parseHexId(tagId, `tag ${tagName || 'unknown'} in namespace ${name || 'unknown'}`),
1226
+ name: tagName,
1227
+ }));
1228
+
1229
+ return { id: hexId, name, tags: hexTags };
1230
+ };
1231
+
1232
+ const parseDeviceTypeXml = (xmlContent, contextLabel) => {
1233
+ const deviceTypeMatch = xmlContent.match(/<deviceType\b[^>]*>/i);
1234
+ if (!deviceTypeMatch) {
1235
+ throw new Error(`Unable to locate device type definition in ${contextLabel}.`);
1236
+ }
1237
+
1238
+ const deviceTypeAttributes = parseAttributes(deviceTypeMatch[0]);
1239
+ const { id, name, revision } = deviceTypeAttributes;
1240
+
1241
+ if (!name) {
1242
+ throw new Error(`Device type name is missing in ${contextLabel}.`);
1243
+ }
1244
+
1245
+ if (!id) {
1246
+ console.log(`Skipping ${contextLabel} (device type "${name}") because it does not declare an id.`);
1247
+ return null;
1248
+ }
1249
+
1250
+ if (!revision) {
1251
+ throw new Error(`Device type revision is missing for ${name}.`);
1252
+ }
1253
+
1254
+ const revisionValue = Number.parseInt(revision, 10);
1255
+ if (Number.isNaN(revisionValue)) {
1256
+ throw new Error(`Invalid revision "${revision}" for device type ${name}.`);
1257
+ }
1258
+
1259
+ const revisionMatches = [...xmlContent.matchAll(/<revision\b[^>]*>/gi)];
1260
+ const revisionHistory = revisionMatches.map((match) => {
1261
+ const revisionAttributes = parseAttributes(match[0]);
1262
+ const { revision: revisionAttr, summary = '' } = revisionAttributes;
1263
+
1264
+ if (!revisionAttr) {
1265
+ throw new Error(`Revision entry missing revision attribute for device type ${name}.`);
1266
+ }
1267
+
1268
+ const revisionNumber = Number.parseInt(revisionAttr, 10);
1269
+ if (Number.isNaN(revisionNumber)) {
1270
+ throw new Error(`Invalid revision value "${revisionAttr}" in revision history for device type ${name}.`);
1271
+ }
1272
+
1273
+ return {
1274
+ revision: revisionNumber,
1275
+ summary,
1276
+ };
1277
+ });
1278
+
1279
+ const classificationMatch = xmlContent.match(/<classification\b[^>]*>/i);
1280
+ if (!classificationMatch) {
1281
+ throw new Error(`Missing classification block for device type ${name}.`);
1282
+ }
1283
+
1284
+ const classificationAttributes = parseAttributes(classificationMatch[0]);
1285
+ const classificationClass = classificationAttributes.class;
1286
+ const { scope } = classificationAttributes;
1287
+
1288
+ if (!classificationClass || !scope) {
1289
+ throw new Error(`Classification attributes are incomplete for device type ${name}.`);
1290
+ }
1291
+
1292
+ const conditionsMatch = xmlContent.match(/<conditions\b[^>]*>([\s\S]*?)<\/conditions>/i);
1293
+ const conditions = [];
1294
+
1295
+ if (conditionsMatch) {
1296
+ const [, conditionsBody] = conditionsMatch;
1297
+ const conditionTagMatches = [...conditionsBody.matchAll(/<condition\b[^>]*>/gi)];
1298
+
1299
+ for (const match of conditionTagMatches) {
1300
+ const conditionAttributes = parseAttributes(match[0]);
1301
+ const { name: conditionName, summary = '', ...rest } = conditionAttributes;
1302
+
1303
+ if (!conditionName) {
1304
+ throw new Error(`Condition without name encountered in device type ${name}.`);
1305
+ }
1306
+
1307
+ conditions.push({ name: conditionName, summary, ...rest });
1308
+ }
1309
+ }
1310
+
1311
+ const clusterMatches = [...xmlContent.matchAll(/<cluster\b[^>]*?(?:\/>|>[\s\S]*?<\/cluster>)/gi)];
1312
+ const clusters = clusterMatches.map((match) => {
1313
+ const fullCluster = match[0];
1314
+ const openingTagMatch = fullCluster.match(/<cluster\b[^>]*>/i);
1315
+
1316
+ if (!openingTagMatch) {
1317
+ throw new Error(`Unable to parse cluster definition for device type ${name}.`);
1318
+ }
1319
+
1320
+ const clusterAttributes = parseAttributes(openingTagMatch[0]);
1321
+ const { id: clusterId, name: clusterName, side } = clusterAttributes;
1322
+
1323
+ if (!clusterId || !clusterName || !side) {
1324
+ throw new Error(`Cluster definition is incomplete in device type ${name}.`);
1325
+ }
1326
+
1327
+ let clusterBody = '';
1328
+
1329
+ if (!fullCluster.trimEnd().endsWith('/>')) {
1330
+ clusterBody = fullCluster.slice(openingTagMatch[0].length, fullCluster.length - '</cluster>'.length);
1331
+ }
1332
+
1333
+ const isMandatory = /^\s*(?:<!--[\s\S]*?-->\s*)*<mandatoryConform\b/i.test(clusterBody);
1334
+
1335
+ const features = {};
1336
+ const featuresMatch = clusterBody.match(/<features\b[^>]*>([\s\S]*?)<\/features>/i);
1337
+
1338
+ if (featuresMatch) {
1339
+ const [, featuresBody] = featuresMatch;
1340
+ const featureMatches = [...featuresBody.matchAll(/<feature\b[^>]*?(?:\/>|>[\s\S]*?<\/feature>)/gi)];
1341
+
1342
+ for (const featureMatch of featureMatches) {
1343
+ const fullFeature = featureMatch[0];
1344
+ const featureOpeningTagMatch = fullFeature.match(/<feature\b[^>]*>/i);
1345
+
1346
+ if (!featureOpeningTagMatch) {
1347
+ throw new Error(`Unable to parse feature definition for cluster ${clusterName} in device type ${name}.`);
1348
+ }
1349
+
1350
+ const featureAttributes = parseAttributes(featureOpeningTagMatch[0]);
1351
+ const { code = '', name: featureName, summary = '', ...rest } = featureAttributes;
1352
+
1353
+ if (!featureName) {
1354
+ throw new Error(`Feature without a name encountered in cluster ${clusterName} for device type ${name}.`);
1355
+ }
1356
+
1357
+ let featureBody = '';
1358
+ if (!fullFeature.trimEnd().endsWith('/>')) {
1359
+ featureBody = fullFeature.slice(featureOpeningTagMatch[0].length, fullFeature.length - '</feature>'.length);
1360
+ }
1361
+
1362
+ const conformance = [];
1363
+ if (/<mandatoryConform\b/i.test(featureBody)) {
1364
+ conformance.push('mandatory');
1365
+ }
1366
+ if (/<optionalConform\b/i.test(featureBody)) {
1367
+ conformance.push('optional');
1368
+ }
1369
+ if (/<disallowConform\b/i.test(featureBody)) {
1370
+ conformance.push('disallow');
1371
+ }
1372
+ if (/<provisionalConform\b/i.test(featureBody)) {
1373
+ conformance.push('provisional');
1374
+ }
1375
+ if (/<deprecateConform\b/i.test(featureBody)) {
1376
+ conformance.push('deprecate');
1377
+ }
1378
+ if (/<otherwiseConform\b/i.test(featureBody)) {
1379
+ conformance.push('otherwise');
1380
+ }
1381
+
1382
+ const featureConditions = [];
1383
+ const featureConditionMatches = [...featureBody.matchAll(/<condition\b[^>]*>/gi)];
1384
+
1385
+ for (const conditionMatch of featureConditionMatches) {
1386
+ const conditionAttributes = parseAttributes(conditionMatch[0]);
1387
+ const { name: conditionName, summary = '', ...conditionRest } = conditionAttributes;
1388
+
1389
+ if (!conditionName) {
1390
+ throw new Error(`Condition without a name referenced in feature ${featureName} of cluster ${clusterName} for device type ${name}.`);
1391
+ }
1392
+
1393
+ const conditionOutput = { name: conditionName };
1394
+
1395
+ if (summary) {
1396
+ conditionOutput.summary = summary;
1397
+ }
1398
+
1399
+ const restEntries = Object.entries(conditionRest);
1400
+ if (restEntries.length > 0) {
1401
+ conditionOutput.attributes = Object.fromEntries(restEntries);
1402
+ }
1403
+
1404
+ featureConditions.push(conditionOutput);
1405
+ }
1406
+
1407
+ const featureOutput = {
1408
+ name: featureName,
1409
+ code,
1410
+ };
1411
+
1412
+ if (summary) {
1413
+ featureOutput.summary = summary;
1414
+ }
1415
+
1416
+ const extraAttributes = Object.entries(rest);
1417
+ if (extraAttributes.length > 0) {
1418
+ featureOutput.attributes = Object.fromEntries(extraAttributes);
1419
+ }
1420
+
1421
+ if (conformance.length > 0) {
1422
+ featureOutput.conformance = conformance;
1423
+ }
1424
+
1425
+ if (featureConditions.length > 0) {
1426
+ featureOutput.conditions = featureConditions;
1427
+ }
1428
+
1429
+ const featureKey = featureName;
1430
+
1431
+ if (features[featureKey]) {
1432
+ console.log(`Warning: Duplicate feature name "${featureKey}" encountered in cluster ${clusterName} for device type ${name}. Overwriting previous entry.`);
1433
+ }
1434
+
1435
+ features[featureKey] = featureOutput;
1436
+ }
1437
+ }
1438
+
1439
+ return {
1440
+ id: parseHexId(clusterId, `cluster ${clusterName} in device type ${name}`),
1441
+ name: normalizeDisplayName(clusterName),
1442
+ side,
1443
+ mandatory: isMandatory,
1444
+ ...(Object.keys(features).length > 0 ? { features } : {}),
1445
+ };
1446
+ });
1447
+
1448
+ return {
1449
+ name,
1450
+ id: parseHexId(id, `device type ${name}`),
1451
+ revision: revisionValue,
1452
+ revisionHistory,
1453
+ conditions,
1454
+ class: classificationClass,
1455
+ scope,
1456
+ clusters,
1457
+ };
1458
+ };
1459
+
1460
+ const parseClusterXml = (xmlContent, contextLabel) => {
1461
+ const clusterMatch = xmlContent.match(/<cluster\b[^>]*>/i);
1462
+ if (!clusterMatch) {
1463
+ throw new Error(`Unable to locate cluster definition in ${contextLabel}.`);
1464
+ }
1465
+
1466
+ const clusterAttributes = parseAttributes(clusterMatch[0]);
1467
+ const { id, name, revision } = clusterAttributes;
1468
+
1469
+ if (!name) {
1470
+ throw new Error(`Cluster name is missing in ${contextLabel}.`);
1471
+ }
1472
+
1473
+ if (!revision) {
1474
+ throw new Error(`Cluster revision is missing in ${contextLabel}.`);
1475
+ }
1476
+
1477
+ const revisionValue = Number.parseInt(revision, 10);
1478
+ if (Number.isNaN(revisionValue)) {
1479
+ throw new Error(`Invalid revision \\"${revision}\\" for cluster ${name} in ${contextLabel}.`);
1480
+ }
1481
+
1482
+ const revisionHistoryMatch = xmlContent.match(/<revisionHistory\b[^>]*>([\s\S]*?)<\/revisionHistory>/i);
1483
+ const revisionHistory = [];
1484
+
1485
+ if (revisionHistoryMatch) {
1486
+ const [, revisionHistoryBody] = revisionHistoryMatch;
1487
+ const revisionMatches = [...revisionHistoryBody.matchAll(/<revision\b[^>]*>/gi)];
1488
+
1489
+ for (const revisionMatchEntry of revisionMatches) {
1490
+ const revisionAttributes = parseAttributes(revisionMatchEntry[0]);
1491
+ const { revision: revisionAttr, summary = '' } = revisionAttributes;
1492
+
1493
+ if (!revisionAttr) {
1494
+ throw new Error(`Revision entry missing revision attribute for cluster ${name}.`);
1495
+ }
1496
+
1497
+ const revisionNumber = Number.parseInt(revisionAttr, 10);
1498
+ if (Number.isNaN(revisionNumber)) {
1499
+ throw new Error(`Invalid revision value \\"${revisionAttr}\\" in revision history for cluster ${name}.`);
1500
+ }
1501
+
1502
+ revisionHistory.push({
1503
+ revision: revisionNumber,
1504
+ summary,
1505
+ });
1506
+ }
1507
+ }
1508
+
1509
+ const classificationMatch = xmlContent.match(/<classification\b[^>]*>/i);
1510
+ if (!classificationMatch) {
1511
+ throw new Error(`Missing classification block for cluster ${name}.`);
1512
+ }
1513
+
1514
+ const classificationAttributes = parseAttributes(classificationMatch[0]);
1515
+ const classification = { ...classificationAttributes };
1516
+
1517
+ const features = {};
1518
+ const featuresMatch = xmlContent.match(/<features\b[^>]*>([\s\S]*?)<\/features>/i);
1519
+
1520
+ if (featuresMatch) {
1521
+ const [, featuresBody] = featuresMatch;
1522
+ const featureMatches = [...featuresBody.matchAll(/<feature\b[^>]*?(?:\/>|>[\s\S]*?<\/feature>)/gi)];
1523
+
1524
+ for (const featureMatch of featureMatches) {
1525
+ const fullFeature = featureMatch[0];
1526
+ const featureOpeningTagMatch = fullFeature.match(/<feature\b[^>]*>/i);
1527
+
1528
+ if (!featureOpeningTagMatch) {
1529
+ throw new Error(`Unable to parse feature definition for cluster ${name} in ${contextLabel}.`);
1530
+ }
1531
+
1532
+ const featureAttributes = parseAttributes(featureOpeningTagMatch[0]);
1533
+ const { code = '', name: featureName, summary = '', bit, ...rest } = featureAttributes;
1534
+
1535
+ if (!featureName) {
1536
+ throw new Error(`Feature without a name encountered in cluster ${name} (${contextLabel}).`);
1537
+ }
1538
+
1539
+ let featureBody = '';
1540
+ if (!fullFeature.trimEnd().endsWith('/>')) {
1541
+ featureBody = fullFeature.slice(featureOpeningTagMatch[0].length, fullFeature.length - '</feature>'.length);
1542
+ }
1543
+
1544
+ const conformance = [];
1545
+ if (/<mandatoryConform\b/i.test(featureBody)) {
1546
+ conformance.push('mandatory');
1547
+ }
1548
+ if (/<optionalConform\b/i.test(featureBody)) {
1549
+ conformance.push('optional');
1550
+ }
1551
+ if (/<disallowConform\b/i.test(featureBody)) {
1552
+ conformance.push('disallow');
1553
+ }
1554
+ if (/<provisionalConform\b/i.test(featureBody)) {
1555
+ conformance.push('provisional');
1556
+ }
1557
+ if (/<deprecateConform\b/i.test(featureBody)) {
1558
+ conformance.push('deprecate');
1559
+ }
1560
+ if (/<otherwiseConform\b/i.test(featureBody)) {
1561
+ conformance.push('otherwise');
1562
+ }
1563
+
1564
+ const featureConditions = [];
1565
+ const featureConditionMatches = [...featureBody.matchAll(/<condition\b[^>]*>/gi)];
1566
+
1567
+ for (const conditionMatch of featureConditionMatches) {
1568
+ const conditionAttributes = parseAttributes(conditionMatch[0]);
1569
+ const { name: conditionName, summary: conditionSummary = '', ...conditionRest } = conditionAttributes;
1570
+
1571
+ if (!conditionName) {
1572
+ throw new Error(`Condition without a name referenced in feature ${featureName} of cluster ${name} (${contextLabel}).`);
1573
+ }
1574
+
1575
+ const conditionOutput = { name: conditionName };
1576
+
1577
+ if (conditionSummary) {
1578
+ conditionOutput.summary = conditionSummary;
1579
+ }
1580
+
1581
+ const restEntries = Object.entries(conditionRest);
1582
+ if (restEntries.length > 0) {
1583
+ conditionOutput.attributes = Object.fromEntries(restEntries);
1584
+ }
1585
+
1586
+ featureConditions.push(conditionOutput);
1587
+ }
1588
+
1589
+ const featureOutput = {
1590
+ name: featureName,
1591
+ code,
1592
+ };
1593
+
1594
+ if (bit !== undefined) {
1595
+ const normalizedBit = bit.trim();
1596
+ let numericBit;
1597
+
1598
+ if (/^0x[0-9a-f]+$/i.test(normalizedBit)) {
1599
+ numericBit = Number.parseInt(normalizedBit, 16);
1600
+ } else if (/^[0-9]+$/.test(normalizedBit)) {
1601
+ numericBit = Number.parseInt(normalizedBit, 10);
1602
+ }
1603
+
1604
+ featureOutput.bit = Number.isNaN(numericBit) || numericBit === undefined ? normalizedBit : numericBit;
1605
+ }
1606
+
1607
+ if (summary) {
1608
+ featureOutput.summary = summary;
1609
+ }
1610
+
1611
+ const extraAttributes = Object.entries(rest);
1612
+ if (extraAttributes.length > 0) {
1613
+ featureOutput.attributes = Object.fromEntries(extraAttributes);
1614
+ }
1615
+
1616
+ if (conformance.length > 0) {
1617
+ featureOutput.conformance = conformance;
1618
+ }
1619
+
1620
+ if (featureConditions.length > 0) {
1621
+ featureOutput.conditions = featureConditions;
1622
+ }
1623
+
1624
+ const featureKey = featureName;
1625
+
1626
+ if (features[featureKey]) {
1627
+ console.log(`Warning: Duplicate feature name "${featureKey}" encountered in cluster ${name} (${contextLabel}). Overwriting previous entry.`);
1628
+ }
1629
+
1630
+ features[featureKey] = featureOutput;
1631
+ }
1632
+ }
1633
+
1634
+ const parsedId = id ? parseHexId(id, `cluster ${name}`) : 0;
1635
+
1636
+ if (!id) {
1637
+ console.log(`Cluster ${name} in ${contextLabel} does not declare an id; defaulting to 0.`);
1638
+ }
1639
+
1640
+ const dataTypes = parseDataTypesBlock(xmlContent, name, contextLabel);
1641
+ const attributes = parseAttributesBlock(xmlContent, name, contextLabel);
1642
+ const commands = parseCommandsBlock(xmlContent, name, contextLabel);
1643
+ const events = parseEventsBlock(xmlContent, name, contextLabel);
1644
+ const hasDataTypes = Object.keys(dataTypes).length > 0;
1645
+ const hasAttributes = Object.keys(attributes).length > 0;
1646
+ const hasCommands = Object.keys(commands).length > 0;
1647
+ const hasEvents = Object.keys(events).length > 0;
1648
+
1649
+ return {
1650
+ id: parsedId,
1651
+ definitionName: name,
1652
+ revision: revisionValue,
1653
+ revisionHistory,
1654
+ classification,
1655
+ ...(Object.keys(features).length > 0 ? { features } : {}),
1656
+ ...(hasDataTypes ? { dataTypes } : {}),
1657
+ ...(hasAttributes ? { attributes } : {}),
1658
+ ...(hasCommands ? { commands } : {}),
1659
+ ...(hasEvents ? { events } : {}),
1660
+ };
1661
+ };
1662
+
1663
+ /** Fetch all namespaces */
1664
+
1665
+ const namespacesFiles = [
1666
+ 'Namespace-Common-Area.xml',
1667
+ 'Namespace-Common-Closure.xml',
1668
+ 'Namespace-Common-CompassDirection.xml',
1669
+ 'Namespace-Common-CompassLocation.xml',
1670
+ 'Namespace-Common-Direction.xml',
1671
+ 'Namespace-Common-Landmark.xml',
1672
+ 'Namespace-Common-Level.xml',
1673
+ 'Namespace-Common-Location.xml',
1674
+ 'Namespace-Common-Number.xml',
1675
+ 'Namespace-Common-Position.xml',
1676
+ 'Namespace-Common-RelativePosition.xml',
1677
+ 'Namespace-ElectricalMeasurement.xml',
1678
+ 'Namespace-Laundry.xml',
1679
+ 'Namespace-PowerSource.xml',
1680
+ 'Namespace-Refrigerator.xml',
1681
+ 'Namespace-RoomAirConditioner.xml',
1682
+ 'Namespace-Switches.xml',
1683
+ ];
1684
+ const namespacesOutput = {};
1685
+
1686
+ console.log(`Fetching namespaces for Matter data model v${MATTER_DATA_MODEL_VERSION}`);
1687
+
1688
+ for (const filename of namespacesFiles) {
1689
+ console.log(`Downloading ${filename}...`);
1690
+ const xml = await fetchRemoteText(`${DATA_MODEL_PATHS.namespaces}${filename}`, {
1691
+ description: `namespace ${filename}`,
1692
+ });
1693
+ const { id, name, tags } = parseNamespaceXml(xml);
1694
+ const key = sanitizeKey(name);
1695
+ namespacesOutput[key] = { id, name, tags };
1696
+ }
1697
+
1698
+ await mkdir(DST_PATH, { recursive: true });
1699
+
1700
+ const outputPath = join(DST_PATH, OUTPUT_NAMESPACES);
1701
+ const sortedNamespaces = Object.fromEntries(Object.entries(namespacesOutput).sort(([left], [right]) => left.localeCompare(right)));
1702
+ const serializedNamespaces = `${JSON.stringify(sortedNamespaces, null, 2)}\n`;
1703
+ await writeFile(outputPath, serializedNamespaces);
1704
+ console.log(`Namespaces JSON written to ${outputPath} (${Object.keys(namespacesOutput).length} entries).`);
1705
+
1706
+ /** Fetch all device types */
1707
+
1708
+ console.log(`Fetching device type identifiers for Matter data model v${MATTER_DATA_MODEL_VERSION}`);
1709
+ const deviceTypeIds = await fetchJson(DATA_MODEL_PATHS.deviceTypesIds, {
1710
+ description: 'device type ids',
1711
+ });
1712
+ const deviceTypeIdEntries = Object.entries(deviceTypeIds);
1713
+ console.log(`Fetched ${deviceTypeIdEntries.length} device type identifier entries.`);
1714
+
1715
+ console.log('Listing available device type XML files...');
1716
+ const deviceTypeDirectoryEntries = await fetchJson(DEVICE_TYPES_DIRECTORY_API, {
1717
+ description: 'device type directory listing',
1718
+ headers: GITHUB_API_HEADERS,
1719
+ });
1720
+
1721
+ if (!Array.isArray(deviceTypeDirectoryEntries)) {
1722
+ throw new Error('Unexpected response while listing device type files.');
1723
+ }
1724
+
1725
+ const deviceTypeFiles = deviceTypeDirectoryEntries.filter((entry) => entry.type === 'file' && typeof entry.name === 'string' && entry.name.toLowerCase().endsWith('.xml'));
1726
+ console.log(`Discovered ${deviceTypeFiles.length} device type XML file(s).`);
1727
+
1728
+ const parsedDeviceTypes = new Map();
1729
+
1730
+ for (const { name: filename, download_url: downloadUrl } of deviceTypeFiles) {
1731
+ if (!downloadUrl) {
1732
+ console.log(`Skipping ${filename} because it does not expose a download URL.`);
1733
+ continue;
1734
+ }
1735
+
1736
+ console.log(`Downloading ${filename}...`);
1737
+ const xml = await fetchRemoteText(downloadUrl, {
1738
+ description: `device type ${filename}`,
1739
+ });
1740
+
1741
+ const parsed = parseDeviceTypeXml(xml, filename);
1742
+ if (!parsed) {
1743
+ continue;
1744
+ }
1745
+
1746
+ if (parsedDeviceTypes.has(parsed.name)) {
1747
+ console.log(`Warning: Duplicate device type definition detected for ${parsed.name}. Overwriting previous entry.`);
1748
+ }
1749
+
1750
+ parsedDeviceTypes.set(parsed.name, parsed);
1751
+ }
1752
+
1753
+ const deviceTypesOutput = {};
1754
+ const classScopeSet = new Set();
1755
+
1756
+ const sortedDeviceTypeIds = deviceTypeIdEntries
1757
+ .map(([rawId, displayName]) => ({
1758
+ id: Number.parseInt(rawId, 10),
1759
+ name: displayName,
1760
+ }))
1761
+ .filter(({ id, name }) => {
1762
+ if (Number.isNaN(id)) {
1763
+ console.log(`Skipping device type id entry with non-numeric id "${name}".`);
1764
+ return false;
1765
+ }
1766
+ return true;
1767
+ })
1768
+ .sort((left, right) => left.id - right.id);
1769
+
1770
+ for (const { id, name } of sortedDeviceTypeIds) {
1771
+ const parsed = parsedDeviceTypes.get(name);
1772
+
1773
+ if (!parsed) {
1774
+ console.log(`Warning: No device type XML found for "${name}" (id ${id}).`);
1775
+ continue;
1776
+ }
1777
+
1778
+ if (parsed.id !== id) {
1779
+ console.log(`Warning: Device type id mismatch for "${name}": ids.json=${id}, xml=${parsed.id}.`);
1780
+ }
1781
+
1782
+ const key = sanitizeKey(name);
1783
+ classScopeSet.add(`${parsed.class}:${parsed.scope}`);
1784
+ deviceTypesOutput[key] = {
1785
+ name: normalizeDisplayName(name),
1786
+ id: parsed.id,
1787
+ revision: parsed.revision,
1788
+ revisionHistory: parsed.revisionHistory,
1789
+ conditions: parsed.conditions,
1790
+ class: parsed.class,
1791
+ scope: parsed.scope,
1792
+ clusters: parsed.clusters,
1793
+ };
1794
+
1795
+ parsedDeviceTypes.delete(name);
1796
+ }
1797
+
1798
+ if (parsedDeviceTypes.size > 0) {
1799
+ console.log(`Including ${parsedDeviceTypes.size} additional device type definition(s) not listed in device_type_ids.json.`);
1800
+ const remainingEntries = [...parsedDeviceTypes.entries()].sort(([leftName], [rightName]) => leftName.localeCompare(rightName));
1801
+
1802
+ for (const [name, parsed] of remainingEntries) {
1803
+ const key = sanitizeKey(name);
1804
+ classScopeSet.add(`${parsed.class}:${parsed.scope}`);
1805
+ deviceTypesOutput[key] = {
1806
+ name: normalizeDisplayName(name),
1807
+ id: parsed.id,
1808
+ revision: parsed.revision,
1809
+ revisionHistory: parsed.revisionHistory,
1810
+ conditions: parsed.conditions,
1811
+ class: parsed.class,
1812
+ scope: parsed.scope,
1813
+ clusters: parsed.clusters,
1814
+ };
1815
+ }
1816
+ }
1817
+
1818
+ const deviceTypesOutputPath = join(DST_PATH, OUTPUT_DEVICE_TYPES);
1819
+ const sortedDeviceTypes = Object.fromEntries(Object.entries(deviceTypesOutput).sort(([left], [right]) => left.localeCompare(right)));
1820
+ const serializedDeviceTypes = `${JSON.stringify(sortedDeviceTypes, null, 2)}\n`;
1821
+ await writeFile(deviceTypesOutputPath, serializedDeviceTypes);
1822
+ console.log(`Device types JSON written to ${deviceTypesOutputPath} (${Object.keys(deviceTypesOutput).length} entries).`);
1823
+ console.log('Observed class/scope combinations:', [...classScopeSet.values()]);
1824
+
1825
+ /** Fetch all clusters */
1826
+
1827
+ console.log(`Fetching cluster identifiers for Matter data model v${MATTER_DATA_MODEL_VERSION}`);
1828
+ const clusterIds = await fetchJson(DATA_MODEL_PATHS.clustersIds, {
1829
+ description: 'cluster ids',
1830
+ });
1831
+
1832
+ const clusterIdEntries = Object.entries(clusterIds)
1833
+ .map(([rawId, name]) => ({
1834
+ id: Number.parseInt(rawId, 10),
1835
+ name,
1836
+ }))
1837
+ .filter(({ id, name }) => {
1838
+ if (Number.isNaN(id)) {
1839
+ console.log(`Skipping cluster id entry with non-numeric id "${name}".`);
1840
+ return false;
1841
+ }
1842
+ return true;
1843
+ })
1844
+ .sort((left, right) => left.id - right.id);
1845
+
1846
+ console.log(`Fetched ${clusterIdEntries.length} cluster identifier entries.`);
1847
+
1848
+ console.log('Listing available cluster XML files...');
1849
+ const clusterDirectoryEntries = await fetchJson(CLUSTERS_DIRECTORY_API, {
1850
+ description: 'cluster directory listing',
1851
+ headers: GITHUB_API_HEADERS,
1852
+ });
1853
+
1854
+ if (!Array.isArray(clusterDirectoryEntries)) {
1855
+ throw new Error('Unexpected response while listing cluster files.');
1856
+ }
1857
+
1858
+ const clusterFiles = clusterDirectoryEntries.filter((entry) => entry.type === 'file' && typeof entry.name === 'string' && entry.name.toLowerCase().endsWith('.xml'));
1859
+ console.log(`Discovered ${clusterFiles.length} cluster XML file(s).`);
1860
+
1861
+ const parsedClustersById = new Map();
1862
+
1863
+ for (const { name: filename, download_url: downloadUrl } of clusterFiles) {
1864
+ if (!downloadUrl) {
1865
+ console.log(`Skipping ${filename} because it does not expose a download URL.`);
1866
+ continue;
1867
+ }
1868
+
1869
+ console.log(`Downloading ${filename}...`);
1870
+ const xml = await fetchRemoteText(downloadUrl, {
1871
+ description: `cluster ${filename}`,
1872
+ });
1873
+
1874
+ const parsed = parseClusterXml(xml, filename);
1875
+
1876
+ if (!parsed) {
1877
+ continue;
1878
+ }
1879
+
1880
+ const existing = parsedClustersById.get(parsed.id);
1881
+ if (existing) {
1882
+ existing.push(parsed);
1883
+ } else {
1884
+ parsedClustersById.set(parsed.id, [parsed]);
1885
+ }
1886
+ }
1887
+
1888
+ const clusterFallbackTemplates = [
1889
+ { target: 'HEPA Filter Monitoring', template: 'Resource Monitoring Clusters' },
1890
+ { target: 'Activated Carbon Filter Monitoring', template: 'Resource Monitoring Clusters' },
1891
+ { target: 'Water Tank Level Monitoring', template: 'Resource Monitoring Clusters' },
1892
+ { target: 'Relative Humidity Measurement', template: 'Water Content Measurement Clusters' },
1893
+ { target: 'Carbon Monoxide Concentration Measurement', template: 'Concentration Measurement Clusters' },
1894
+ { target: 'Carbon Dioxide Concentration Measurement', template: 'Concentration Measurement Clusters' },
1895
+ { target: 'Nitrogen Dioxide Concentration Measurement', template: 'Concentration Measurement Clusters' },
1896
+ { target: 'Ozone Concentration Measurement', template: 'Concentration Measurement Clusters' },
1897
+ { target: 'PM2.5 Concentration Measurement', template: 'Concentration Measurement Clusters' },
1898
+ { target: 'Formaldehyde Concentration Measurement', template: 'Concentration Measurement Clusters' },
1899
+ { target: 'PM1 Concentration Measurement', template: 'Concentration Measurement Clusters' },
1900
+ { target: 'PM10 Concentration Measurement', template: 'Concentration Measurement Clusters' },
1901
+ { target: 'Total Volatile Organic Compounds Concentration Measurement', template: 'Concentration Measurement Clusters' },
1902
+ { target: 'Radon Concentration Measurement', template: 'Concentration Measurement Clusters' },
1903
+ ];
1904
+
1905
+ const clusterFallbackTemplateMap = new Map(clusterFallbackTemplates.map(({ target, template }) => [sanitizeKey(target), template]));
1906
+
1907
+ const clustersOutput = {};
1908
+ for (const { id, name } of clusterIdEntries) {
1909
+ const key = sanitizeKey(name);
1910
+ const entry = {
1911
+ name: normalizeDisplayName(name),
1912
+ id,
1913
+ };
1914
+
1915
+ if (clustersOutput[key]) {
1916
+ console.log(`Warning: Duplicate sanitized cluster name detected for "${name}".`);
1917
+ }
1918
+
1919
+ const parsedBuckets = parsedClustersById.get(id);
1920
+
1921
+ if (!parsedBuckets || parsedBuckets.length === 0) {
1922
+ console.log(`Warning: No cluster XML found for id ${id} (${name}).`);
1923
+
1924
+ const fallbackTemplate = clusterFallbackTemplateMap.get(key);
1925
+ if (fallbackTemplate === 'Concentration Measurement Clusters') {
1926
+ console.log('Using default ConcentrationMeasurement metadata.');
1927
+ } else if (fallbackTemplate === 'Water Content Measurement Clusters') {
1928
+ console.log('Using default WaterContentMeasurement metadata.');
1929
+ }
1930
+ } else {
1931
+ const parsed = parsedBuckets.shift();
1932
+ entry.revision = parsed.revision;
1933
+ entry.revisionHistory = parsed.revisionHistory;
1934
+ entry.classification = parsed.classification;
1935
+ if (parsed.features) {
1936
+ entry.features = parsed.features;
1937
+ }
1938
+ if (parsed.dataTypes) {
1939
+ entry.dataTypes = parsed.dataTypes;
1940
+ }
1941
+ if (parsed.attributes) {
1942
+ entry.attributes = parsed.attributes;
1943
+ }
1944
+ if (parsed.commands) {
1945
+ entry.commands = parsed.commands;
1946
+ }
1947
+ if (parsed.events) {
1948
+ entry.events = parsed.events;
1949
+ }
1950
+
1951
+ if (parsedBuckets.length === 0) {
1952
+ parsedClustersById.delete(id);
1953
+ } else {
1954
+ parsedClustersById.set(id, parsedBuckets);
1955
+ }
1956
+ }
1957
+
1958
+ clustersOutput[key] = entry;
1959
+ }
1960
+
1961
+ if (parsedClustersById.size > 0) {
1962
+ const remainingClusters = [];
1963
+ for (const bucket of parsedClustersById.values()) {
1964
+ remainingClusters.push(...bucket);
1965
+ }
1966
+
1967
+ remainingClusters.sort((left, right) => left.id - right.id || left.definitionName.localeCompare(right.definitionName));
1968
+
1969
+ console.log(`Including ${remainingClusters.length} additional cluster definition(s) not listed in cluster_ids.json.`);
1970
+ for (const parsed of remainingClusters) {
1971
+ console.log(` - ${parsed.definitionName} (id ${parsed.id})`);
1972
+ }
1973
+
1974
+ for (const parsed of remainingClusters) {
1975
+ const key = sanitizeKey(parsed.definitionName);
1976
+
1977
+ if (clustersOutput[key]) {
1978
+ console.log(`Warning: Duplicate sanitized cluster name detected for additional cluster "${parsed.definitionName}".`);
1979
+ }
1980
+
1981
+ clustersOutput[key] = {
1982
+ name: normalizeDisplayName(parsed.definitionName),
1983
+ id: parsed.id,
1984
+ revision: parsed.revision,
1985
+ revisionHistory: parsed.revisionHistory,
1986
+ classification: parsed.classification,
1987
+ ...(parsed.features ? { features: parsed.features } : {}),
1988
+ ...(parsed.dataTypes ? { dataTypes: parsed.dataTypes } : {}),
1989
+ ...(parsed.attributes ? { attributes: parsed.attributes } : {}),
1990
+ ...(parsed.commands ? { commands: parsed.commands } : {}),
1991
+ ...(parsed.events ? { events: parsed.events } : {}),
1992
+ };
1993
+ }
1994
+ }
1995
+
1996
+ for (const { target, template } of clusterFallbackTemplates) {
1997
+ const targetKey = sanitizeKey(target);
1998
+ const templateKey = sanitizeKey(template);
1999
+
2000
+ const targetEntry = clustersOutput[targetKey];
2001
+ const templateEntry = clustersOutput[templateKey];
2002
+
2003
+ if (!targetEntry) {
2004
+ console.log(`Warning: Unable to apply fallback for missing cluster "${target}" because it was not found in cluster ids.`);
2005
+ continue;
2006
+ }
2007
+
2008
+ if (!templateEntry) {
2009
+ console.log(`Warning: Unable to apply fallback for missing cluster "${target}" because template "${template}" is unavailable.`);
2010
+ continue;
2011
+ }
2012
+
2013
+ if (targetEntry.revision) {
2014
+ continue;
2015
+ }
2016
+
2017
+ console.log(`Applying fallback metadata for cluster "${target}" using template "${template}".`);
2018
+ const updatedEntry = {
2019
+ ...targetEntry,
2020
+ revision: templateEntry.revision,
2021
+ };
2022
+
2023
+ if (templateEntry.revisionHistory) {
2024
+ updatedEntry.revisionHistory = cloneDeep(templateEntry.revisionHistory);
2025
+ }
2026
+
2027
+ if (templateEntry.classification) {
2028
+ updatedEntry.classification = cloneDeep(templateEntry.classification);
2029
+ }
2030
+
2031
+ if (templateEntry.features) {
2032
+ updatedEntry.features = cloneDeep(templateEntry.features);
2033
+ }
2034
+
2035
+ if (templateEntry.dataTypes) {
2036
+ updatedEntry.dataTypes = cloneDeep(templateEntry.dataTypes);
2037
+ }
2038
+
2039
+ if (templateEntry.attributes) {
2040
+ updatedEntry.attributes = cloneDeep(templateEntry.attributes);
2041
+ }
2042
+
2043
+ if (templateEntry.commands) {
2044
+ updatedEntry.commands = cloneDeep(templateEntry.commands);
2045
+ }
2046
+
2047
+ if (templateEntry.events) {
2048
+ updatedEntry.events = cloneDeep(templateEntry.events);
2049
+ }
2050
+
2051
+ clustersOutput[targetKey] = updatedEntry;
2052
+ }
2053
+
2054
+ const clustersOutputPath = join(DST_PATH, OUTPUT_CLUSTERS);
2055
+ const sortedClusters = Object.fromEntries(Object.entries(clustersOutput).sort(([left], [right]) => left.localeCompare(right)));
2056
+ const serializedClusters = `${JSON.stringify(sortedClusters, null, 2)}\n`;
2057
+ await writeFile(clustersOutputPath, serializedClusters);
2058
+ console.log(`Cluster identifiers JSON written to ${clustersOutputPath} (${Object.keys(sortedClusters).length} entries).`);