miravo 0.6.0 → 0.6.1

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 +7 -70
  3. package/bin/miravo.js +84 -0
  4. package/package.json +20 -36
  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,459 +0,0 @@
1
- /**
2
- * Litmus Edge template export (JSON + tags CSV) from desired state.
3
- *
4
- * The JSON template includes hardcoded `driverId` per protocol to support direct upload to LE.
5
- * The CSV template uses `driverName` since that is expected by the LE CSV import format.
6
- */
7
-
8
- import type { ProtocolName } from "@miravo/shared";
9
- import type { ApplyDesiredState, DesiredDevice, DesiredTag } from "./le-apply.ts";
10
- import { buildLeDeviceHubDeviceProperties } from "./le-devicehub-properties.ts";
11
- import { LE_DRIVER_IDS, LE_DRIVER_NAMES } from "./le-drivers.ts";
12
- import { encodeDeviceMarkers } from "./le-markers.ts";
13
-
14
- const ZERO = "00000000-0000-0000-0000-000000000000";
15
-
16
- interface LeExportResult {
17
- readonly templateJson: string;
18
- readonly tagsCSV: string;
19
- }
20
-
21
- /**
22
- * Convert offline desired state into a full LE template JSON + tags CSV.
23
- *
24
- * The JSON includes devicehub devices with registers, DT models and instances,
25
- * and stats config. The CSV covers all tags across all devices in one file,
26
- * with deviceId/registerId cross-referenced from the generated template.
27
- */
28
- export function buildLeExport(desired: ApplyDesiredState, templateId: string): LeExportResult {
29
- const generatedDevices = desired.devices.map((d) => generateDevice(d, templateId));
30
- const modelObjects = extractModelObjects(desired, templateId);
31
- const modelByName = new Map(
32
- modelObjects.map((m) => [(m as Record<string, unknown>)["name"] as string, m]),
33
- );
34
- const instanceObjects = desired.instances.map((inst) => {
35
- const model = modelByName.get(inst.dtModelName);
36
- if (!model) throw new Error(`Model "${inst.dtModelName}" not found in desired state`);
37
- return buildInstance(
38
- crypto.randomUUID(), // LE requires a UUID, not the human-readable instanceId
39
- inst.dtInstanceName,
40
- inst.modelName,
41
- inst.boundDeviceName,
42
- templateId,
43
- model as Record<string, unknown>,
44
- inst.browsePath,
45
- );
46
- });
47
-
48
- const template = {
49
- devicehub: { Devs: generatedDevices.map((g) => g.devObj) },
50
- dtwin: { models: modelObjects, instances: instanceObjects },
51
- stats: buildStats(generatedDevices),
52
- };
53
-
54
- return {
55
- templateJson: JSON.stringify(template, null, 2),
56
- tagsCSV: buildTagsCSV(generatedDevices),
57
- };
58
- }
59
-
60
- interface GeneratedDevice {
61
- readonly id: string;
62
- readonly name: string;
63
- readonly instanceId: string;
64
- readonly modelName: string;
65
- readonly protocol: ProtocolName;
66
- readonly registers: GeneratedRegister[];
67
- readonly devObj: unknown;
68
- }
69
-
70
- interface GeneratedRegister {
71
- readonly id: string;
72
- readonly tag: DesiredTag;
73
- readonly protocol: ProtocolName;
74
- }
75
-
76
- function generateDevice(device: DesiredDevice, templateId: string): GeneratedDevice {
77
- const deviceId = crypto.randomUUID();
78
- const registers: GeneratedRegister[] = device.tags.map((tag) => ({
79
- id: crypto.randomUUID(),
80
- tag,
81
- protocol: device.protocol,
82
- }));
83
-
84
- const markers = encodeDeviceMarkers({
85
- templateId,
86
- instanceId: device.instanceId,
87
- modelName: device.modelName,
88
- protocol: device.protocol,
89
- unsPath: device.browsePath,
90
- });
91
-
92
- const devObj = {
93
- Dev: {
94
- id: deviceId,
95
- name: device.deviceName,
96
- driverId: LE_DRIVER_IDS[device.protocol],
97
- settings: buildDeviceSettings(device),
98
- aliasTopics: true,
99
- meta_tags: Object.fromEntries(markers.map(({ Key, Value }) => [Key, Value])),
100
- debug: false,
101
- stopped: true,
102
- version: "0.0.1",
103
- },
104
- Registers: registers.map((r) => buildRegisterObj(r, deviceId)),
105
- };
106
-
107
- return {
108
- id: deviceId,
109
- name: device.deviceName,
110
- instanceId: device.instanceId,
111
- modelName: device.modelName,
112
- protocol: device.protocol,
113
- registers,
114
- devObj,
115
- };
116
- }
117
-
118
- function buildDeviceSettings(device: DesiredDevice): Record<string, string> {
119
- const { host, port } = device.endpoint;
120
- return Object.fromEntries(
121
- buildLeDeviceHubDeviceProperties(device.protocol, host, port).map((item) => [
122
- item.Key,
123
- item.Value,
124
- ]),
125
- );
126
- }
127
-
128
- function buildRegisterObj(reg: GeneratedRegister, deviceId: string): Record<string, unknown> {
129
- return {
130
- id: reg.id,
131
- deviceId,
132
- value_type: reg.tag.valueType,
133
- name: reg.tag.name,
134
- tag_name: reg.tag.tagName,
135
- settings: buildRegisterSettings(reg.tag, reg.protocol),
136
- };
137
- }
138
-
139
- function buildRegisterSettings(tag: DesiredTag, protocol: ProtocolName): Record<string, string> {
140
- if (protocol === "modbus") {
141
- const propMap = new Map(tag.properties.map((p) => [p.Key, p.Value]));
142
- return {
143
- address: propMap.get("address") ?? "0",
144
- count: propMap.get("count") ?? "1",
145
- pollingInterval: "1000",
146
- };
147
- }
148
- return {};
149
- }
150
-
151
- /**
152
- * Adapt leDtJson models for /dm/template/v2 import. The ImportModel mutation
153
- * and the template import use different subsets of the model schema:
154
- *
155
- * - version: must be absent (0 causes "restore failed")
156
- * - dynamicAttributes.topicExpression: must be absent (template rejects it; instances carry it)
157
- * - parameters: must include id/ownerId/ownerType (template rejects simple {name,value} form)
158
- * - staticAttributes: $param.* references and __miravo_placeholder__ are resolved/sanitized
159
- */
160
- function extractModelObjects(desired: ApplyDesiredState, templateId: string): unknown[] {
161
- return desired.models.flatMap((m) => {
162
- const arr = m.leDtJson as unknown[];
163
- const models = Array.isArray(arr) ? arr : [arr];
164
- return models.map((model) => {
165
- if (!model || typeof model !== "object") return model;
166
- const { version: _v, ...rest } = model as Record<string, unknown>;
167
- const modelId = rest["id"] as string;
168
- const modelNameLower = String(rest["name"] ?? "").toLowerCase();
169
-
170
- const dynAttrs = rest["dynamicAttributes"] as Array<Record<string, unknown>> | undefined;
171
- if (dynAttrs) {
172
- rest["dynamicAttributes"] = dynAttrs.map(
173
- ({ topicExpression: _te, topic: _t, ...attr }) => ({
174
- ...attr,
175
- topic: "",
176
- unit: sanitizeUnit(String(attr["unit"] ?? "")),
177
- }),
178
- );
179
- }
180
-
181
- const params = rest["parameters"] as Array<Record<string, unknown>> | undefined;
182
- if (params?.[0] && !params[0]["id"]) {
183
- rest["parameters"] = params.map((p) => ({
184
- id: crypto.randomUUID(),
185
- ownerId: modelId,
186
- ownerType: 2,
187
- name: p["name"],
188
- value: p["value"],
189
- }));
190
- }
191
-
192
- const staticAttrs = rest["staticAttributes"] as Array<Record<string, unknown>> | undefined;
193
- if (staticAttrs) {
194
- rest["staticAttributes"] = staticAttrs.map((a) => {
195
- const key = String(a["key"] ?? "");
196
- let value = String(a["value"] ?? "");
197
- // Resolve miravo marker placeholders at model level where possible
198
- if (value === "__miravo_placeholder__") {
199
- if (key === "miravo_template") value = templateId;
200
- else if (key === "miravo_model") value = modelNameLower;
201
- // miravo_instance cannot be resolved at model level — keep placeholder
202
- }
203
- // Template-exported models are Miravo-owned — mark as managed so cleanup can target them
204
- if (key === "miravo_managed") value = "true";
205
- // LE rejects $param.* template references and empty values
206
- if (value.startsWith("$")) value = "N/A";
207
- return { ...a, value };
208
- });
209
- }
210
-
211
- return rest;
212
- });
213
- });
214
- }
215
-
216
- function buildInstance(
217
- instanceId: string,
218
- instanceName: string,
219
- modelName: string,
220
- deviceName: string,
221
- templateId: string,
222
- model: Record<string, unknown>,
223
- browsePath: string,
224
- ): Record<string, unknown> {
225
- const modelId = model["id"] as string;
226
- const modelNameLower = (model["name"] as string).toLowerCase();
227
-
228
- const modelDynAttrs = (model["dynamicAttributes"] as Array<Record<string, unknown>>) ?? [];
229
- const modelStaticAttrs = (model["staticAttributes"] as Array<Record<string, unknown>>) ?? [];
230
- const modelParams = (model["parameters"] as Array<Record<string, unknown>>) ?? [];
231
-
232
- const dynamicAttributes = modelDynAttrs.map((a) => {
233
- const name = String(a["name"] ?? "");
234
- // The LE template import format rejects `topicExpression` on model objects
235
- // (extractModelObjects strips it), but instance objects require it.
236
- // Reconstruct it here using the standard devicehub alias formula; the {device}
237
- // parameter is resolved at runtime by LE using the instance's bound device name.
238
- const topicExpression =
239
- (a["topicExpression"] as string | undefined) || `devicehub.alias.{device}.${name}`;
240
- return {
241
- id: crypto.randomUUID(),
242
- modelId: ZERO,
243
- parentId: a["id"] as string,
244
- instanceId,
245
- topic: "",
246
- name,
247
- unit: sanitizeUnit(String(a["unit"] ?? "")),
248
- dataType: a["dataType"] ?? "",
249
- schemaId: ZERO,
250
- skipParentKey: a["skipParentKey"] ?? false,
251
- topicExpression,
252
- };
253
- });
254
-
255
- const staticAttributes = modelStaticAttrs.map((a) => {
256
- const key = String(a["key"] ?? "");
257
- let value: string;
258
- switch (key) {
259
- case "miravo_managed":
260
- value = "true";
261
- break;
262
- case "miravo_template":
263
- value = templateId;
264
- break;
265
- case "miravo_model":
266
- value = modelName;
267
- break;
268
- case "miravo_instance":
269
- value = instanceName;
270
- break;
271
- case "uns_path":
272
- value = browsePath;
273
- break;
274
- default:
275
- value = sanitizeStaticValue(String(a["value"] ?? ""));
276
- }
277
- return {
278
- id: crypto.randomUUID(),
279
- parentId: a["id"] as string,
280
- modelId: ZERO,
281
- instanceId,
282
- key,
283
- value,
284
- type: a["type"] ?? 0,
285
- };
286
- });
287
-
288
- // Override model's {device} parameter with the bound device name.
289
- const parameters = modelParams.map((p) => ({
290
- id: crypto.randomUUID(),
291
- ownerId: instanceId,
292
- ownerType: 1,
293
- name: p["name"] as string,
294
- value: p["name"] === "device" ? deviceName : (p["value"] as string),
295
- }));
296
-
297
- return {
298
- id: instanceId,
299
- modelId,
300
- name: instanceName,
301
- topic: `digitaltwins.${modelNameLower}.${instanceName}`,
302
- interval: 1,
303
- image: "",
304
- dynamicAttributes,
305
- staticAttributes,
306
- transformationBackup: null,
307
- parameters,
308
- flatten: false,
309
- isEnabled: true,
310
- publishAttributes: false,
311
- };
312
- }
313
-
314
- function buildStats(devices: GeneratedDevice[]): Record<string, unknown> {
315
- const statsDevices = devices.map((d) => ({
316
- name: d.name,
317
- id: d.id,
318
- retention_hours: 720,
319
- save_all: true,
320
- topic: `devicehub.alias.${d.name}.>`,
321
- }));
322
-
323
- const timeseries = devices.map((d) => ({
324
- id: d.id,
325
- name: d.name,
326
- topics: [`devicehub.alias.${d.name}.>`],
327
- }));
328
-
329
- return {
330
- devices: statsDevices,
331
- timeseries,
332
- timeseries_db: { source: "internal", enabled: true },
333
- };
334
- }
335
-
336
- /** LE DeviceHub CSV header — matches the real LE export format. */
337
- const CSV_HEADER =
338
- "deviceId,deviceName,driverName,name,tagName,description,valueType,registerId,publishCoV,tagFormula,metaData,Retain,Qos,address,count,pollingInterval,endianness";
339
-
340
- function buildTagsCSV(devices: GeneratedDevice[]): string {
341
- const rows = [CSV_HEADER];
342
- for (const device of devices) {
343
- for (const reg of device.registers) {
344
- rows.push(buildCSVRow(device, reg));
345
- }
346
- }
347
- return `${rows.join("\n")}\n`;
348
- }
349
-
350
- function buildCSVRow(device: GeneratedDevice, reg: GeneratedRegister): string {
351
- const { id: deviceId, name: deviceName, instanceId, modelName, protocol } = device;
352
- const { tag, id: registerId } = reg;
353
- const driverName = LE_DRIVER_NAMES[protocol];
354
- const propMap = new Map(tag.properties.map((p) => [p.Key, p.Value]));
355
- const metaData = buildTagMetaDataJson(tag, instanceId, modelName);
356
-
357
- switch (protocol) {
358
- case "opcua":
359
- return [
360
- deviceId,
361
- esc(deviceName),
362
- esc(driverName),
363
- esc(tag.name), // OPC-UA NodeId
364
- esc(tag.tagName),
365
- "",
366
- tag.valueType,
367
- registerId,
368
- "false",
369
- "",
370
- metaData,
371
- "",
372
- "",
373
- "",
374
- "",
375
- "",
376
- "",
377
- ].join(",");
378
-
379
- case "modbus":
380
- return [
381
- deviceId,
382
- esc(deviceName),
383
- esc(driverName),
384
- "H", // Holding register
385
- esc(tag.tagName),
386
- "",
387
- tag.valueType,
388
- registerId,
389
- "false",
390
- "",
391
- metaData,
392
- "",
393
- "",
394
- propMap.get("address") ?? "0",
395
- propMap.get("count") ?? "1",
396
- "1.0000000000",
397
- "AB CD",
398
- ].join(",");
399
-
400
- case "mqtt":
401
- return [
402
- deviceId,
403
- esc(deviceName),
404
- esc(driverName),
405
- esc(tag.name), // MQTT topic
406
- esc(tag.tagName),
407
- "",
408
- tag.valueType,
409
- registerId,
410
- "false",
411
- "",
412
- metaData,
413
- "Disable",
414
- "0",
415
- "",
416
- "",
417
- "",
418
- "",
419
- ].join(",");
420
-
421
- default: {
422
- const _never: never = protocol;
423
- throw new Error(`Unknown protocol: ${_never}`);
424
- }
425
- }
426
- }
427
-
428
- function buildTagMetaDataJson(tag: DesiredTag, instanceId: string, modelName: string): string {
429
- const meta: Record<string, string> = {
430
- uns_path: tag.unsPath,
431
- model: modelName,
432
- instance: instanceId,
433
- access: tag.access,
434
- };
435
- if (tag.unit) meta["unit"] = tag.unit;
436
- if (tag.euRange) {
437
- meta["eu_range_min"] = String(tag.euRange[0]);
438
- meta["eu_range_max"] = String(tag.euRange[1]);
439
- }
440
- return esc(JSON.stringify(meta));
441
- }
442
-
443
- function esc(value: string): string {
444
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
445
- return `"${value.replace(/"/g, '""')}"`;
446
- }
447
- return value;
448
- }
449
-
450
- /** LE rejects staticAttribute values that are empty or have leading/trailing spaces. */
451
- function sanitizeStaticValue(value: string): string {
452
- const trimmed = value.trim();
453
- return trimmed.length > 0 ? trimmed : "N/A";
454
- }
455
-
456
- /** LE rejects dynamicAttribute units containing /, ^, or %. */
457
- function sanitizeUnit(unit: string): string {
458
- return unit.replace(/[/^%]/g, "");
459
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Miravo → LE value type mappings (shared between export and apply).
3
- */
4
-
5
- /** OPC UA: map Miravo data types to LE OPC UA Client Advanced value types. */
6
- export const OPCUA_VALUE_TYPE: Readonly<Record<string, string>> = {
7
- Boolean: "bool",
8
- Int32: "int32",
9
- UInt16: "uint16",
10
- UInt32: "uint32",
11
- Float: "float32",
12
- Double: "float64",
13
- String: "string",
14
- };
15
-
16
- /** Modbus: map Miravo register encodings to LE Modbus TCP value types. */
17
- export const MODBUS_VALUE_TYPE: Readonly<Record<string, string>> = {
18
- float32: "float32",
19
- int32: "int32",
20
- uint32: "uint32",
21
- uint16: "word",
22
- boolean: "word",
23
- };
@@ -1,115 +0,0 @@
1
- import { resolve } from "node:path";
2
- import { loadTemplate } from "@miravo/core";
3
- import { DEFAULT_MQTT_PORT, MiravoError } from "@miravo/shared";
4
- import type { MqttClient } from "mqtt";
5
- import { connect } from "mqtt";
6
-
7
- interface BrokerConnectionInfo {
8
- readonly url: string;
9
- readonly port: number;
10
- readonly username?: string | undefined;
11
- readonly password?: string | undefined;
12
- }
13
-
14
- /**
15
- * Resolve broker connection info from a template file or defaults.
16
- * Checks the template's `protocols.mqtt` config for an external URL or port.
17
- * Falls back to the default embedded broker at `mqtt://127.0.0.1:1883`.
18
- */
19
- async function resolveBrokerInfo(templatePath?: string): Promise<BrokerConnectionInfo> {
20
- if (templatePath) {
21
- const resolved = resolve(templatePath);
22
- const template = await loadTemplate(resolved);
23
- const mqttConfig = template.protocols?.mqtt;
24
-
25
- const username = mqttConfig?.username;
26
- const password = mqttConfig?.password;
27
-
28
- if (mqttConfig?.external) {
29
- const port = mqttConfig.port ?? DEFAULT_MQTT_PORT;
30
- return { url: mqttConfig.external, port, username, password };
31
- }
32
-
33
- const port = mqttConfig?.port ?? DEFAULT_MQTT_PORT;
34
- return { url: `mqtt://127.0.0.1:${port}`, port, username, password };
35
- }
36
-
37
- return { url: `mqtt://127.0.0.1:${DEFAULT_MQTT_PORT}`, port: DEFAULT_MQTT_PORT };
38
- }
39
-
40
- /**
41
- * Create an MQTT client and wait for connection with a timeout.
42
- * Rejects with a MiravoError containing an actionable fix message.
43
- */
44
- function connectWithTimeout(
45
- url: string,
46
- timeoutMs: number,
47
- credentials?: { username: string; password?: string | undefined },
48
- ): Promise<MqttClient> {
49
- return new Promise<MqttClient>((resolve, reject) => {
50
- const client = connect(url, {
51
- connectTimeout: timeoutMs,
52
- reconnectPeriod: 0, // No auto-reconnect for CLI commands
53
- ...(credentials !== undefined && {
54
- username: credentials.username,
55
- ...(credentials.password !== undefined && { password: credentials.password }),
56
- }),
57
- });
58
-
59
- const timer = setTimeout(() => {
60
- client.end(true);
61
- reject(
62
- new MiravoError(
63
- `Cannot connect to MQTT broker at ${url} within ${timeoutMs}ms.`,
64
- "Is a simulation running? Start one with 'miravo start'.",
65
- ),
66
- );
67
- }, timeoutMs);
68
-
69
- client.on("connect", () => {
70
- clearTimeout(timer);
71
- resolve(client);
72
- });
73
-
74
- client.once("error", (err) => {
75
- clearTimeout(timer);
76
- client.end(true);
77
- reject(
78
- new MiravoError(
79
- `MQTT connection error: ${err.message}`,
80
- `Check that a simulation is running on ${url}. Start one with 'miravo start'.`,
81
- ),
82
- );
83
- });
84
- });
85
- }
86
-
87
- /**
88
- * Resolve broker info from a template and connect with credentials.
89
- * Convenience wrapper around resolveBrokerInfo + connectWithTimeout.
90
- */
91
- async function connectToBroker(
92
- templatePath: string | undefined,
93
- timeoutMs: number,
94
- ): Promise<{ client: MqttClient; brokerInfo: BrokerConnectionInfo }> {
95
- const brokerInfo = await resolveBrokerInfo(templatePath);
96
- const credentials =
97
- brokerInfo.username !== undefined
98
- ? { username: brokerInfo.username, password: brokerInfo.password }
99
- : undefined;
100
- const client = await connectWithTimeout(brokerInfo.url, timeoutMs, credentials);
101
- return { client, brokerInfo };
102
- }
103
-
104
- /**
105
- * Safely disconnect an MQTT client. Never throws.
106
- */
107
- function disconnectClient(client: MqttClient): void {
108
- try {
109
- client.end(true);
110
- } catch {
111
- // Ignore disconnect errors
112
- }
113
- }
114
-
115
- export { connectToBroker, disconnectClient };