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.
- package/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- 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
|
-
};
|
package/src/lib/mqtt-client.ts
DELETED
|
@@ -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 };
|