miravo 0.6.0 → 0.6.2

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 (187) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +29 -59
  3. package/bin/miravo.js +84 -0
  4. package/package.json +21 -37
  5. package/console-dist/assets/activity-DsWeRq4h.js +0 -1
  6. package/console-dist/assets/api-4AqKvsm9.js +0 -1
  7. package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
  8. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
  9. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
  10. package/console-dist/assets/box-C1WaoOMF.js +0 -1
  11. package/console-dist/assets/card-BjeP32PN.js +0 -1
  12. package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
  13. package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
  14. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
  15. package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
  16. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
  17. package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
  18. package/console-dist/assets/control-CPhvwx9o.js +0 -1
  19. package/console-dist/assets/control-Cg2up9NA.js +0 -1
  20. package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
  21. package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
  22. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
  23. package/console-dist/assets/dist-BhbDML0a.js +0 -1
  24. package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
  25. package/console-dist/assets/download-DQ6NH16U.js +0 -1
  26. package/console-dist/assets/engine-store-CrLnz692.js +0 -1
  27. package/console-dist/assets/events-CXm29Ok4.js +0 -1
  28. package/console-dist/assets/events-D01IxhnP.js +0 -1
  29. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  30. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  31. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  32. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  33. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  34. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  36. package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
  37. package/console-dist/assets/fleet-lotMGVqe.js +0 -1
  38. package/console-dist/assets/format-kXTonpDJ.js +0 -1
  39. package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
  40. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
  41. package/console-dist/assets/index-CVPKSDvJ.js +0 -12
  42. package/console-dist/assets/index-DJkgDeun.css +0 -2
  43. package/console-dist/assets/input-BWEQFptx.js +0 -1
  44. package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
  45. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  46. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  47. package/console-dist/assets/label-uephX5ZG.js +0 -1
  48. package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
  49. package/console-dist/assets/login-C3RJli0m.js +0 -126
  50. package/console-dist/assets/models-B_UioWQ5.js +0 -1
  51. package/console-dist/assets/models-_IPlPiZ7.js +0 -1
  52. package/console-dist/assets/not-found-B8gAm08S.js +0 -1
  53. package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
  54. package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
  55. package/console-dist/assets/platforms-DStNbtVc.js +0 -1
  56. package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
  57. package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
  58. package/console-dist/assets/radio-CoHegOv5.js +0 -1
  59. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  60. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  61. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  62. package/console-dist/assets/select-CeO4vP0G.js +0 -1
  63. package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
  64. package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
  65. package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
  66. package/console-dist/assets/tags-CiXOIHtY.js +0 -4
  67. package/console-dist/assets/tags-FQCXIxrP.js +0 -1
  68. package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
  69. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
  70. package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
  71. package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
  72. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
  73. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
  74. package/console-dist/assets/validation-Blvud9_O.js +0 -1
  75. package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
  76. package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
  77. package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
  78. package/console-dist/favicon.svg +0 -37
  79. package/console-dist/index.html +0 -82
  80. package/console-dist/miravo-logo.png +0 -0
  81. package/src/__tests__/integration/executable-methods-runner.ts +0 -262
  82. package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
  83. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
  84. package/src/commands/adapter.ts +0 -249
  85. package/src/commands/apply.test.ts +0 -62
  86. package/src/commands/apply.ts +0 -216
  87. package/src/commands/cleanup.test.ts +0 -34
  88. package/src/commands/cleanup.ts +0 -201
  89. package/src/commands/cli-test-setup.ts +0 -91
  90. package/src/commands/export.test.ts +0 -51
  91. package/src/commands/export.ts +0 -404
  92. package/src/commands/fork.ts +0 -219
  93. package/src/commands/inject.test.ts +0 -57
  94. package/src/commands/inject.ts +0 -212
  95. package/src/commands/load.ts +0 -167
  96. package/src/commands/logs.ts +0 -173
  97. package/src/commands/models.test.ts +0 -162
  98. package/src/commands/models.ts +0 -338
  99. package/src/commands/pause.ts +0 -67
  100. package/src/commands/platforms.test.ts +0 -186
  101. package/src/commands/platforms.ts +0 -371
  102. package/src/commands/ps.ts +0 -116
  103. package/src/commands/remove.ts +0 -38
  104. package/src/commands/reset.ts +0 -44
  105. package/src/commands/rm.ts +0 -97
  106. package/src/commands/spawn.ts +0 -45
  107. package/src/commands/speed.ts +0 -40
  108. package/src/commands/start/adapter-config.ts +0 -157
  109. package/src/commands/start/catalog-le-dt.test.ts +0 -50
  110. package/src/commands/start/catalog-le-dt.ts +0 -187
  111. package/src/commands/start/catalog-model-schema.ts +0 -89
  112. package/src/commands/start/catalog-template-detail.ts +0 -54
  113. package/src/commands/start/daemon.ts +0 -255
  114. package/src/commands/start/display.ts +0 -150
  115. package/src/commands/start/engine-setup.ts +0 -247
  116. package/src/commands/start/events.ts +0 -126
  117. package/src/commands/start/index.ts +0 -897
  118. package/src/commands/start/protocol-plans.ts +0 -239
  119. package/src/commands/start/template-loader.ts +0 -197
  120. package/src/commands/start/template-resolver.ts +0 -185
  121. package/src/commands/start/types.ts +0 -73
  122. package/src/commands/start.test.ts +0 -480
  123. package/src/commands/status.test.ts +0 -20
  124. package/src/commands/status.ts +0 -470
  125. package/src/commands/stop.test.ts +0 -84
  126. package/src/commands/stop.ts +0 -169
  127. package/src/commands/templates.test.ts +0 -58
  128. package/src/commands/templates.ts +0 -277
  129. package/src/index.ts +0 -166
  130. package/src/lib/admin-auth.ts +0 -157
  131. package/src/lib/admin-client.test.ts +0 -51
  132. package/src/lib/admin-client.ts +0 -326
  133. package/src/lib/admin-platform-routes.ts +0 -1017
  134. package/src/lib/admin-platform-stream.test.ts +0 -434
  135. package/src/lib/admin-platform-stream.ts +0 -788
  136. package/src/lib/admin-responses.ts +0 -211
  137. package/src/lib/admin-server.test.ts +0 -781
  138. package/src/lib/admin-server.ts +0 -797
  139. package/src/lib/auth-store.ts +0 -87
  140. package/src/lib/daemon-registry.test.ts +0 -429
  141. package/src/lib/daemon-registry.ts +0 -343
  142. package/src/lib/le/le-apply.test.ts +0 -591
  143. package/src/lib/le/le-apply.ts +0 -832
  144. package/src/lib/le/le-auth.ts +0 -89
  145. package/src/lib/le/le-cleanup.test.ts +0 -168
  146. package/src/lib/le/le-cleanup.ts +0 -399
  147. package/src/lib/le/le-cli-helpers.ts +0 -77
  148. package/src/lib/le/le-client.ts +0 -184
  149. package/src/lib/le/le-concurrency.test.ts +0 -157
  150. package/src/lib/le/le-concurrency.ts +0 -78
  151. package/src/lib/le/le-connection-test.ts +0 -42
  152. package/src/lib/le/le-devicehub-properties.test.ts +0 -53
  153. package/src/lib/le/le-devicehub-properties.ts +0 -33
  154. package/src/lib/le/le-devicehub.ts +0 -213
  155. package/src/lib/le/le-digital-twins.ts +0 -285
  156. package/src/lib/le/le-drivers.ts +0 -23
  157. package/src/lib/le/le-endpoint.ts +0 -40
  158. package/src/lib/le/le-environments.test.ts +0 -73
  159. package/src/lib/le/le-environments.ts +0 -156
  160. package/src/lib/le/le-markers.ts +0 -87
  161. package/src/lib/le/le-plan.test.ts +0 -26
  162. package/src/lib/le/le-plan.ts +0 -109
  163. package/src/lib/le/le-platform-operations.test.ts +0 -198
  164. package/src/lib/le/le-platform-operations.ts +0 -340
  165. package/src/lib/le/le-static-model-schema.ts +0 -78
  166. package/src/lib/le/le-static-planner.test.ts +0 -255
  167. package/src/lib/le/le-static-planner.ts +0 -442
  168. package/src/lib/le/le-template-exporter.test.ts +0 -527
  169. package/src/lib/le/le-template-exporter.ts +0 -459
  170. package/src/lib/le/le-value-types.ts +0 -23
  171. package/src/lib/mqtt-client.ts +0 -115
  172. package/src/lib/parse.test.ts +0 -123
  173. package/src/lib/parse.ts +0 -87
  174. package/src/lib/resolve-simulation.test.ts +0 -118
  175. package/src/lib/resolve-simulation.ts +0 -181
  176. package/src/lib/session-store.ts +0 -110
  177. package/src/lib/shutdown.test.ts +0 -562
  178. package/src/lib/shutdown.ts +0 -621
  179. package/src/lib/template-category.test.ts +0 -71
  180. package/src/lib/template-category.ts +0 -72
  181. package/src/lib/template-locator.ts +0 -55
  182. package/src/lib/template-utils.ts +0 -45
  183. package/src/lib/ws-hub.ts +0 -281
  184. package/src/logger.test.ts +0 -172
  185. package/src/logger.ts +0 -402
  186. package/src/output.test.ts +0 -64
  187. package/src/output.ts +0 -160
@@ -1,527 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { ApplyDesiredState } from "./le-apply.ts";
3
- import { LE_DRIVER_IDS, LE_DRIVER_NAMES } from "./le-drivers.ts";
4
- import { buildLeExport } from "./le-template-exporter.ts";
5
-
6
- // ---------------------------------------------------------------------------
7
- // Minimal desired state builders
8
- // ---------------------------------------------------------------------------
9
-
10
- function makeOpcuaDevice(): ApplyDesiredState["devices"][number] {
11
- return {
12
- instanceId: "pump-001",
13
- modelName: "pump",
14
- protocol: "opcua",
15
- deviceName: "pump-001",
16
- browsePath: "area/line/pump-001",
17
- endpoint: { host: "miravo.local", port: 4840 },
18
- properties: [
19
- { Key: "ServerURL", Value: "opc.tcp://miravo.local:4840" },
20
- { Key: "SecurityPolicy", Value: "0" },
21
- { Key: "SecurityMode", Value: "0" },
22
- ],
23
- tags: [
24
- {
25
- name: "ns=1;s=area/line/pump-001/rpm",
26
- tagName: "rpm",
27
- valueType: "float32",
28
- properties: [],
29
- unsPath: "area/line/pump-001/rpm",
30
- access: "read",
31
- },
32
- {
33
- name: "ns=1;s=area/line/pump-001/running",
34
- tagName: "running",
35
- valueType: "boolean",
36
- properties: [],
37
- unsPath: "area/line/pump-001/running",
38
- access: "read",
39
- },
40
- ],
41
- };
42
- }
43
-
44
- function makeModbusDevice(): ApplyDesiredState["devices"][number] {
45
- return {
46
- instanceId: "tank-001",
47
- modelName: "tank",
48
- protocol: "modbus",
49
- deviceName: "tank-001",
50
- browsePath: "area/line/tank-001",
51
- endpoint: { host: "miravo.local", port: 502 },
52
- properties: [
53
- { Key: "networkAddress", Value: "miravo.local" },
54
- { Key: "networkPort", Value: "502" },
55
- { Key: "stationId", Value: "1" },
56
- ],
57
- tags: [
58
- {
59
- name: "H",
60
- tagName: "level",
61
- valueType: "float32",
62
- properties: [
63
- { Key: "address", Value: "0" },
64
- { Key: "count", Value: "1" },
65
- ],
66
- unsPath: "area/line/tank-001/level",
67
- access: "read",
68
- },
69
- ],
70
- };
71
- }
72
-
73
- function makeMqttDevice(): ApplyDesiredState["devices"][number] {
74
- return {
75
- instanceId: "motor-001",
76
- modelName: "motor",
77
- protocol: "mqtt",
78
- deviceName: "motor-001",
79
- browsePath: "area/line/motor-001",
80
- endpoint: { host: "miravo.local", port: 1883 },
81
- properties: [
82
- { Key: "BrokerAddress", Value: "miravo.local" },
83
- { Key: "Port", Value: "1883" },
84
- ],
85
- tags: [
86
- {
87
- name: "area/line/motor-001/speed",
88
- tagName: "speed",
89
- valueType: "float32",
90
- properties: [],
91
- unsPath: "area/line/motor-001/speed",
92
- access: "read",
93
- },
94
- ],
95
- };
96
- }
97
-
98
- const PUMP_MODEL_JSON = [
99
- {
100
- id: "aaaaaaaa-0000-0000-0000-000000000001",
101
- name: "PUMP",
102
- version: 3,
103
- dynamicAttributes: [
104
- {
105
- id: "bbbbbbbb-0000-0000-0000-000000000001",
106
- name: "rpm",
107
- dataType: "float",
108
- unit: "rpm",
109
- skipParentKey: false,
110
- topicExpression: "devicehub.alias.{device}.rpm",
111
- topic: "",
112
- },
113
- ],
114
- staticAttributes: [
115
- { id: "cccccccc-0000-0000-0000-000000000001", key: "miravo_managed", value: "true", type: 0 },
116
- {
117
- id: "cccccccc-0000-0000-0000-000000000002",
118
- key: "miravo_template",
119
- value: "__miravo_placeholder__",
120
- type: 0,
121
- },
122
- {
123
- id: "cccccccc-0000-0000-0000-000000000003",
124
- key: "miravo_model",
125
- value: "__miravo_placeholder__",
126
- type: 0,
127
- },
128
- {
129
- id: "cccccccc-0000-0000-0000-000000000004",
130
- key: "miravo_instance",
131
- value: "__miravo_placeholder__",
132
- type: 0,
133
- },
134
- ],
135
- parameters: [{ name: "device", value: "" }],
136
- },
137
- ];
138
-
139
- function makeDesiredState(
140
- devices: ApplyDesiredState["devices"],
141
- modelJson: unknown[] = PUMP_MODEL_JSON,
142
- ): ApplyDesiredState {
143
- const modelName = devices[0]?.modelName ?? "pump";
144
- return {
145
- devices,
146
- models: [{ name: modelName.toUpperCase(), leDtJson: modelJson }],
147
- instances: devices.map((d) => ({
148
- instanceId: d.instanceId,
149
- modelName: d.modelName,
150
- dtInstanceName: d.instanceId,
151
- dtModelName: d.modelName.toUpperCase(),
152
- boundDeviceName: d.deviceName,
153
- browsePath: d.browsePath,
154
- })),
155
- };
156
- }
157
-
158
- // ---------------------------------------------------------------------------
159
- // JSON template structure
160
- // ---------------------------------------------------------------------------
161
-
162
- describe("buildLeExport — JSON template", () => {
163
- test("top-level keys are devicehub, dtwin, stats", () => {
164
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
165
- const tpl = JSON.parse(templateJson) as Record<string, unknown>;
166
- expect(Object.keys(tpl).sort()).toEqual(["devicehub", "dtwin", "stats"]);
167
- });
168
-
169
- test("device uses driverId (not driverName) for opcua", () => {
170
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
171
- const tpl = JSON.parse(templateJson) as {
172
- devicehub: { Devs: Array<{ Dev: Record<string, unknown> }> };
173
- };
174
- const [first] = tpl.devicehub.Devs;
175
- expect(first?.Dev["driverId"]).toBe(LE_DRIVER_IDS.opcua);
176
- expect(first?.Dev["driverName"]).toBeUndefined();
177
- });
178
-
179
- test("device uses driverId for modbus", () => {
180
- const { templateJson } = buildLeExport(
181
- makeDesiredState(
182
- [makeModbusDevice()],
183
- PUMP_MODEL_JSON.map((m) => ({ ...m, name: "TANK" })),
184
- ),
185
- "demo",
186
- );
187
- const tpl = JSON.parse(templateJson) as {
188
- devicehub: { Devs: Array<{ Dev: Record<string, unknown> }> };
189
- };
190
- expect(tpl.devicehub.Devs[0]?.Dev["driverId"]).toBe(LE_DRIVER_IDS.modbus);
191
- });
192
-
193
- test("device uses driverId for mqtt", () => {
194
- const mqttModel = PUMP_MODEL_JSON.map((m) => ({ ...m, name: "MOTOR" }));
195
- const { templateJson } = buildLeExport(makeDesiredState([makeMqttDevice()], mqttModel), "demo");
196
- const tpl = JSON.parse(templateJson) as {
197
- devicehub: { Devs: Array<{ Dev: Record<string, unknown> }> };
198
- };
199
- expect(tpl.devicehub.Devs[0]?.Dev["driverId"]).toBe(LE_DRIVER_IDS.mqtt);
200
- });
201
-
202
- test("registers count matches tag count", () => {
203
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
204
- const tpl = JSON.parse(templateJson) as {
205
- devicehub: { Devs: Array<{ Registers: unknown[] }> };
206
- };
207
- expect(tpl.devicehub.Devs[0]?.Registers).toHaveLength(2);
208
- });
209
-
210
- test("device is stopped by default", () => {
211
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
212
- const tpl = JSON.parse(templateJson) as {
213
- devicehub: { Devs: Array<{ Dev: Record<string, unknown> }> };
214
- };
215
- expect(tpl.devicehub.Devs[0]?.Dev["stopped"]).toBe(true);
216
- });
217
-
218
- test("stats devices match devicehub devices by name", () => {
219
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
220
- const tpl = JSON.parse(templateJson) as {
221
- devicehub: { Devs: Array<{ Dev: { name: string } }> };
222
- stats: { devices: Array<{ name: string }> };
223
- };
224
- const dhNames = tpl.devicehub.Devs.map((d) => d.Dev.name);
225
- const statNames = tpl.stats.devices.map((d) => d.name);
226
- expect(statNames).toEqual(dhNames);
227
- });
228
- });
229
-
230
- // ---------------------------------------------------------------------------
231
- // extractModelObjects — version stripping + parameter upgrade + static values
232
- // ---------------------------------------------------------------------------
233
-
234
- describe("buildLeExport — model transformation", () => {
235
- test("strips version from model objects", () => {
236
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
237
- const tpl = JSON.parse(templateJson) as { dtwin: { models: Array<Record<string, unknown>> } };
238
- const [model] = tpl.dtwin.models;
239
- expect(model?.["version"]).toBeUndefined();
240
- });
241
-
242
- test("upgrades bare {name,value} parameters to full form with id/ownerId/ownerType", () => {
243
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
244
- const tpl = JSON.parse(templateJson) as {
245
- dtwin: { models: Array<{ parameters: Array<Record<string, unknown>> }> };
246
- };
247
- const [model] = tpl.dtwin.models;
248
- const [param] = model?.parameters ?? [];
249
- expect(typeof param?.["id"]).toBe("string");
250
- expect(param?.["ownerType"]).toBe(2);
251
- });
252
-
253
- test("resolves miravo_template placeholder in model static attrs", () => {
254
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "my-template");
255
- const tpl = JSON.parse(templateJson) as {
256
- dtwin: { models: Array<{ staticAttributes: Array<{ key: string; value: string }> }> };
257
- };
258
- const [model] = tpl.dtwin.models;
259
- const templateAttr = model?.staticAttributes.find((a) => a.key === "miravo_template");
260
- expect(templateAttr?.value).toBe("my-template");
261
- });
262
-
263
- test("resolves miravo_model placeholder using lowercased model name", () => {
264
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
265
- const tpl = JSON.parse(templateJson) as {
266
- dtwin: { models: Array<{ staticAttributes: Array<{ key: string; value: string }> }> };
267
- };
268
- const [model] = tpl.dtwin.models;
269
- const modelAttr = model?.staticAttributes.find((a) => a.key === "miravo_model");
270
- expect(modelAttr?.value).toBe("pump");
271
- });
272
-
273
- test("keeps miravo_instance placeholder unresolved at model level", () => {
274
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
275
- const tpl = JSON.parse(templateJson) as {
276
- dtwin: { models: Array<{ staticAttributes: Array<{ key: string; value: string }> }> };
277
- };
278
- const [model] = tpl.dtwin.models;
279
- const instAttr = model?.staticAttributes.find((a) => a.key === "miravo_instance");
280
- // Cannot resolve per-instance value at model level
281
- expect(instAttr?.value).toBe("__miravo_placeholder__");
282
- });
283
-
284
- test("replaces $param.* references with N/A", () => {
285
- const [pumpModelBase] = PUMP_MODEL_JSON;
286
- const modelWithParam = [
287
- {
288
- ...pumpModelBase,
289
- staticAttributes: [{ id: "x", key: "label", value: "$param.device", type: 0 }],
290
- },
291
- ];
292
- const { templateJson } = buildLeExport(
293
- makeDesiredState([makeOpcuaDevice()], modelWithParam),
294
- "demo",
295
- );
296
- const tpl = JSON.parse(templateJson) as {
297
- dtwin: { models: Array<{ staticAttributes: Array<{ key: string; value: string }> }> };
298
- };
299
- expect(tpl.dtwin.models[0]?.staticAttributes[0]?.value).toBe("N/A");
300
- });
301
-
302
- test("strips topicExpression from model dynamicAttributes", () => {
303
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
304
- const tpl = JSON.parse(templateJson) as {
305
- dtwin: { models: Array<{ dynamicAttributes: Array<Record<string, unknown>> }> };
306
- };
307
- const [model] = tpl.dtwin.models;
308
- const [dynAttr] = model?.dynamicAttributes ?? [];
309
- expect(dynAttr?.["topicExpression"]).toBeUndefined();
310
- });
311
-
312
- test("sanitizes unit chars rejected by LE (/, ^, %)", () => {
313
- const [pumpModelBase] = PUMP_MODEL_JSON;
314
- const modelWithBadUnit = [
315
- {
316
- ...pumpModelBase,
317
- dynamicAttributes: [
318
- {
319
- id: "bb",
320
- name: "flow",
321
- dataType: "float",
322
- unit: "m^3/h",
323
- skipParentKey: false,
324
- topic: "",
325
- },
326
- ],
327
- },
328
- ];
329
- const { templateJson } = buildLeExport(
330
- makeDesiredState([makeOpcuaDevice()], modelWithBadUnit),
331
- "demo",
332
- );
333
- const tpl = JSON.parse(templateJson) as {
334
- dtwin: { models: Array<{ dynamicAttributes: Array<{ unit: string }> }> };
335
- };
336
- expect(tpl.dtwin.models[0]?.dynamicAttributes[0]?.unit).toBe("m3h");
337
- });
338
- });
339
-
340
- // ---------------------------------------------------------------------------
341
- // Instance objects
342
- // ---------------------------------------------------------------------------
343
-
344
- describe("buildLeExport — instance objects", () => {
345
- test("instance resolves miravo_instance to the instance name", () => {
346
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
347
- const tpl = JSON.parse(templateJson) as {
348
- dtwin: { instances: Array<{ staticAttributes: Array<{ key: string; value: string }> }> };
349
- };
350
- const instAttr = tpl.dtwin.instances[0]?.staticAttributes.find(
351
- (a) => a.key === "miravo_instance",
352
- );
353
- expect(instAttr?.value).toBe("pump-001");
354
- });
355
-
356
- test("instance topicExpression is reconstructed from devicehub alias formula", () => {
357
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
358
- const tpl = JSON.parse(templateJson) as {
359
- dtwin: {
360
- instances: Array<{ dynamicAttributes: Array<{ topicExpression: string; name: string }> }>;
361
- };
362
- };
363
- const [instance] = tpl.dtwin.instances;
364
- const [dynAttr] = instance?.dynamicAttributes ?? [];
365
- expect(dynAttr?.topicExpression).toBe(`devicehub.alias.{device}.${dynAttr?.name}`);
366
- });
367
-
368
- test("instance {device} parameter is overridden with the bound device name", () => {
369
- const { templateJson } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
370
- const tpl = JSON.parse(templateJson) as {
371
- dtwin: { instances: Array<{ parameters: Array<{ name: string; value: string }> }> };
372
- };
373
- const deviceParam = tpl.dtwin.instances[0]?.parameters.find((p) => p.name === "device");
374
- expect(deviceParam?.value).toBe("pump-001");
375
- });
376
-
377
- test("throws when instance references a missing model", () => {
378
- const desired: ApplyDesiredState = {
379
- devices: [makeOpcuaDevice()],
380
- models: [], // model missing
381
- instances: [
382
- {
383
- instanceId: "pump-001",
384
- modelName: "pump",
385
- dtInstanceName: "pump-001",
386
- dtModelName: "PUMP",
387
- boundDeviceName: "pump-001",
388
- browsePath: "area/line/pump-001",
389
- },
390
- ],
391
- };
392
- expect(() => buildLeExport(desired, "demo")).toThrow('Model "PUMP" not found');
393
- });
394
- });
395
-
396
- // ---------------------------------------------------------------------------
397
- // CSV structure
398
- // ---------------------------------------------------------------------------
399
-
400
- const CSV_COLUMN_COUNT = 17;
401
-
402
- function parseCSVRow(row: string): string[] {
403
- const cols: string[] = [];
404
- let i = 0;
405
- while (i <= row.length) {
406
- if (row[i] === '"') {
407
- let field = "";
408
- i++;
409
- while (i < row.length) {
410
- if (row[i] === '"' && row[i + 1] === '"') {
411
- field += '"';
412
- i += 2;
413
- } else if (row[i] === '"') {
414
- i++;
415
- break;
416
- } else {
417
- field += row[i++];
418
- }
419
- }
420
- cols.push(field);
421
- if (row[i] === ",") i++;
422
- } else {
423
- const end = row.indexOf(",", i);
424
- if (end === -1) {
425
- cols.push(row.slice(i));
426
- break;
427
- }
428
- cols.push(row.slice(i, end));
429
- i = end + 1;
430
- }
431
- }
432
- return cols;
433
- }
434
-
435
- function parseCSV(csv: string): string[][] {
436
- return csv.trim().split("\n").map(parseCSVRow);
437
- }
438
-
439
- describe("buildLeExport — CSV structure", () => {
440
- test("header has 17 columns", () => {
441
- const { tagsCSV } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
442
- const [header] = parseCSV(tagsCSV);
443
- expect(header).toHaveLength(CSV_COLUMN_COUNT);
444
- });
445
-
446
- test("OPC-UA rows have 17 columns", () => {
447
- const { tagsCSV } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
448
- const rows = parseCSV(tagsCSV).slice(1); // skip header
449
- for (const row of rows) {
450
- expect(row).toHaveLength(CSV_COLUMN_COUNT);
451
- }
452
- });
453
-
454
- test("Modbus rows have 17 columns with address and count populated", () => {
455
- const modbusModel = PUMP_MODEL_JSON.map((m) => ({ ...m, name: "TANK" }));
456
- const { tagsCSV } = buildLeExport(makeDesiredState([makeModbusDevice()], modbusModel), "demo");
457
- const rows = parseCSV(tagsCSV).slice(1);
458
- expect(rows).toHaveLength(1);
459
- const [row] = rows;
460
- expect(row).toHaveLength(CSV_COLUMN_COUNT);
461
- // address col (index 13) = "0", count col (index 14) = "1"
462
- expect(row?.[13]).toBe("0");
463
- expect(row?.[14]).toBe("1");
464
- });
465
-
466
- test("MQTT rows have 17 columns", () => {
467
- const mqttModel = PUMP_MODEL_JSON.map((m) => ({ ...m, name: "MOTOR" }));
468
- const { tagsCSV } = buildLeExport(makeDesiredState([makeMqttDevice()], mqttModel), "demo");
469
- const rows = parseCSV(tagsCSV).slice(1);
470
- expect(rows).toHaveLength(1);
471
- expect(rows[0]).toHaveLength(CSV_COLUMN_COUNT);
472
- });
473
-
474
- test("CSV driverName uses human-readable names, not IDs", () => {
475
- const { tagsCSV } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
476
- const rows = parseCSV(tagsCSV).slice(1);
477
- // driverName is col index 2
478
- expect(rows[0]?.[2]).toBe(LE_DRIVER_NAMES.opcua);
479
- });
480
-
481
- test("deviceId in CSV matches the device id in the JSON template", () => {
482
- const { templateJson, tagsCSV } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
483
- const tpl = JSON.parse(templateJson) as { devicehub: { Devs: Array<{ Dev: { id: string } }> } };
484
- const deviceId = tpl.devicehub.Devs[0]?.Dev.id;
485
-
486
- const rows = parseCSV(tagsCSV).slice(1);
487
- // deviceId is col index 0
488
- expect(rows[0]?.[0]).toBe(deviceId);
489
- });
490
-
491
- test("registerId in CSV matches the register id in the JSON template", () => {
492
- const { templateJson, tagsCSV } = buildLeExport(makeDesiredState([makeOpcuaDevice()]), "demo");
493
- const tpl = JSON.parse(templateJson) as {
494
- devicehub: { Devs: Array<{ Registers: Array<{ id: string }> }> };
495
- };
496
- const [firstDev] = tpl.devicehub.Devs;
497
- const registerIds = firstDev?.Registers.map((r) => r.id) ?? [];
498
-
499
- const rows = parseCSV(tagsCSV).slice(1);
500
- // registerId is col index 7
501
- const csvRegisterIds = rows.map((row) => row[7]);
502
- expect(csvRegisterIds).toEqual(registerIds);
503
- });
504
-
505
- test("CSV value with comma is quoted", () => {
506
- const deviceWithComma = {
507
- ...makeOpcuaDevice(),
508
- tags: [
509
- {
510
- name: "ns=1;s=area,line/pump/rpm",
511
- tagName: "rpm",
512
- valueType: "float32",
513
- properties: [],
514
- unsPath: "area,line/pump/rpm",
515
- access: "read" as const,
516
- },
517
- ],
518
- };
519
- const { tagsCSV } = buildLeExport(makeDesiredState([deviceWithComma]), "demo");
520
- const [, row] = parseCSV(tagsCSV);
521
- // With the proper CSV parser, a quoted field still gives exactly 17 columns
522
- expect(row).toHaveLength(CSV_COLUMN_COUNT);
523
- // The raw CSV line must contain a quoted field
524
- const lines = tagsCSV.trim().split("\n");
525
- expect(lines[1]).toContain('"');
526
- });
527
- });