@attraccess/plugin-wago 0.1.0-nightly.954.1 → 0.1.0-nightly.957.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.
package/dist/index.js
CHANGED
|
@@ -52015,7 +52015,242 @@ WagoEnrollment = __decorateClass([
|
|
|
52015
52015
|
|
|
52016
52016
|
// backend/configuration.ts
|
|
52017
52017
|
var import_node_crypto = require("node:crypto");
|
|
52018
|
-
|
|
52018
|
+
|
|
52019
|
+
// modbus/model.ts
|
|
52020
|
+
var registerCount = (format) => ["uint16", "int16"].includes(format.dataType) ? 1 : 2;
|
|
52021
|
+
var base = {
|
|
52022
|
+
addressBase: 0,
|
|
52023
|
+
byteOrder: "big",
|
|
52024
|
+
wordOrder: "big",
|
|
52025
|
+
offset: 0,
|
|
52026
|
+
pollIntervalMs: 5e3,
|
|
52027
|
+
functionCode: 3
|
|
52028
|
+
};
|
|
52029
|
+
var BUILTIN_MODBUS_PROFILES = ["879-3000", "879-1300"].map((model) => ({
|
|
52030
|
+
id: `wago-${model}-unverified`,
|
|
52031
|
+
name: `WAGO ${model} \u2014 UNQUALIFIED / map unverified`,
|
|
52032
|
+
version: 1,
|
|
52033
|
+
actions: [],
|
|
52034
|
+
measurements: [
|
|
52035
|
+
{
|
|
52036
|
+
...base,
|
|
52037
|
+
id: "active-power",
|
|
52038
|
+
name: "Active power",
|
|
52039
|
+
address: 20498,
|
|
52040
|
+
dataType: "float32",
|
|
52041
|
+
scale: 1e3,
|
|
52042
|
+
unit: "watt",
|
|
52043
|
+
kind: "live"
|
|
52044
|
+
},
|
|
52045
|
+
...[
|
|
52046
|
+
{ id: "import-energy", name: "Imported energy", address: 24588 },
|
|
52047
|
+
{ id: "export-energy", name: "Exported energy", address: 24600 }
|
|
52048
|
+
].map((entry) => ({
|
|
52049
|
+
...base,
|
|
52050
|
+
...entry,
|
|
52051
|
+
dataType: model === "879-3000" ? "float32" : "uint32",
|
|
52052
|
+
scale: model === "879-3000" ? 1e3 : 1,
|
|
52053
|
+
unit: "watt-hour",
|
|
52054
|
+
kind: "cumulative"
|
|
52055
|
+
}))
|
|
52056
|
+
]
|
|
52057
|
+
}));
|
|
52058
|
+
function freeze(value) {
|
|
52059
|
+
Object.values(value).forEach((child) => {
|
|
52060
|
+
if (child && typeof child === "object") freeze(child);
|
|
52061
|
+
});
|
|
52062
|
+
Object.freeze(value);
|
|
52063
|
+
}
|
|
52064
|
+
freeze(BUILTIN_MODBUS_PROFILES);
|
|
52065
|
+
function findProfile(config, device) {
|
|
52066
|
+
return [...BUILTIN_MODBUS_PROFILES, ...config.profiles].find(
|
|
52067
|
+
(p) => p.id === device.profileId && p.version === device.profileVersion
|
|
52068
|
+
);
|
|
52069
|
+
}
|
|
52070
|
+
function validateModbus(value) {
|
|
52071
|
+
const errors = [];
|
|
52072
|
+
const fail = (path, message) => errors.push({ path, code: "invalid_modbus", message });
|
|
52073
|
+
const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
52074
|
+
const integer = (v, min, max) => Number.isSafeInteger(v) && Number(v) >= min && Number(v) <= max;
|
|
52075
|
+
const name = (v) => typeof v === "string" && !!v.trim() && v.length <= 160;
|
|
52076
|
+
const keys = (v, allowed, path) => {
|
|
52077
|
+
for (const key of Object.keys(v)) if (!allowed.includes(key)) fail(`${path}.${key}`, "unknown field");
|
|
52078
|
+
};
|
|
52079
|
+
if (!object(value)) {
|
|
52080
|
+
fail("modbus", "must be an object");
|
|
52081
|
+
return errors;
|
|
52082
|
+
}
|
|
52083
|
+
keys(value, ["connections", "devices", "profiles"], "modbus");
|
|
52084
|
+
const arrays = ["connections", "devices", "profiles"];
|
|
52085
|
+
for (const key of arrays)
|
|
52086
|
+
if (!Array.isArray(value[key]) || value[key].length > 64)
|
|
52087
|
+
fail(`modbus.${key}`, "must be an array with at most 64 entries");
|
|
52088
|
+
if (errors.length) return errors;
|
|
52089
|
+
const config = value;
|
|
52090
|
+
for (const key of arrays) {
|
|
52091
|
+
const ids = /* @__PURE__ */ new Set();
|
|
52092
|
+
config[key].forEach((entry, i) => {
|
|
52093
|
+
if (!object(entry) || !name(entry.id) || ids.has(String(entry.id)))
|
|
52094
|
+
fail(`modbus.${key}[${i}]`, "unique non-empty ID required");
|
|
52095
|
+
if (entry && typeof entry.id === "string") ids.add(entry.id);
|
|
52096
|
+
});
|
|
52097
|
+
}
|
|
52098
|
+
if (errors.length) return errors;
|
|
52099
|
+
const endpoints = /* @__PURE__ */ new Set();
|
|
52100
|
+
config.connections.forEach((c, i) => {
|
|
52101
|
+
const path = `modbus.connections[${i}]`;
|
|
52102
|
+
keys(
|
|
52103
|
+
c,
|
|
52104
|
+
[
|
|
52105
|
+
"id",
|
|
52106
|
+
"timeoutMs",
|
|
52107
|
+
"reconnectMs",
|
|
52108
|
+
"queueLimit",
|
|
52109
|
+
...c.transport === "tcp" ? ["transport", "host", "port"] : ["transport", "path", "baudRate", "parity", "stopBits"]
|
|
52110
|
+
],
|
|
52111
|
+
path
|
|
52112
|
+
);
|
|
52113
|
+
if (!integer(c.timeoutMs, 10, 6e4) || !integer(c.reconnectMs, 0, 6e4) || !integer(c.queueLimit, 1, 128))
|
|
52114
|
+
fail(path, "timeout 10..60000ms, reconnect 0..60000ms, queue limit 1..128 required");
|
|
52115
|
+
let endpoint;
|
|
52116
|
+
if (c.transport === "tcp") {
|
|
52117
|
+
if (!name(c.host) || /[\s/]/.test(c.host) || !integer(c.port, 1, 65535))
|
|
52118
|
+
return void fail(path, "TCP host and port 1..65535 required");
|
|
52119
|
+
endpoint = `tcp:${c.host.toLowerCase()}:${c.port}`;
|
|
52120
|
+
} else if (c.transport === "rtu") {
|
|
52121
|
+
if (typeof c.path !== "string" || !/^\/dev\/[a-zA-Z0-9_./-]+$/.test(c.path) || c.path.includes("..") || c.path.split("/").slice(2).some((segment) => segment === "" || segment === ".") || ![1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200].includes(c.baudRate) || !["none", "even", "odd"].includes(c.parity) || ![1, 2].includes(c.stopBits))
|
|
52122
|
+
return void fail(path, "canonical serial device path, valid baud, parity and stop bits required");
|
|
52123
|
+
endpoint = `rtu:${c.path}`;
|
|
52124
|
+
} else return void fail(path, "transport must be tcp or rtu");
|
|
52125
|
+
if (endpoints.has(endpoint)) fail(path, "share one connection for devices on the same endpoint");
|
|
52126
|
+
endpoints.add(endpoint);
|
|
52127
|
+
});
|
|
52128
|
+
const format = (f, path) => {
|
|
52129
|
+
if (![0, 1].includes(f.addressBase) || !Number.isSafeInteger(f.address) || !integer(f.address - f.addressBase, 0, 65536 - registerCount(f)) || !["uint16", "int16", "uint32", "int32", "float32"].includes(f.dataType) || !["big", "little"].includes(f.byteOrder) || !["big", "little"].includes(f.wordOrder) || !Number.isFinite(f.scale) || f.scale === 0 || !Number.isFinite(f.offset))
|
|
52130
|
+
fail(path, "invalid address, dtype, order or transform");
|
|
52131
|
+
};
|
|
52132
|
+
config.profiles.forEach((p, i) => {
|
|
52133
|
+
const path = `modbus.profiles[${i}]`;
|
|
52134
|
+
keys(p, ["id", "name", "version", "measurements", "actions"], path);
|
|
52135
|
+
if (BUILTIN_MODBUS_PROFILES.some((b) => b.id === p.id) || !name(p.name) || !integer(p.version, 1, 1e6))
|
|
52136
|
+
fail(path, "custom ID, name and positive version required; built-ins are immutable");
|
|
52137
|
+
if (!Array.isArray(p.measurements) || !Array.isArray(p.actions) || p.measurements.length + p.actions.length > 128) {
|
|
52138
|
+
fail(path, "measurements/actions arrays required, maximum 128 entries");
|
|
52139
|
+
return;
|
|
52140
|
+
}
|
|
52141
|
+
const ids = /* @__PURE__ */ new Set();
|
|
52142
|
+
[...p.measurements, ...p.actions].forEach((f, j) => {
|
|
52143
|
+
if (!object(f) || !name(f.id) || !name(f.name) || ids.has(f.id)) {
|
|
52144
|
+
fail(`${path}[${j}]`, "unique named entry required");
|
|
52145
|
+
return;
|
|
52146
|
+
}
|
|
52147
|
+
ids.add(f.id);
|
|
52148
|
+
format(f, `${path}.${f.id}`);
|
|
52149
|
+
keys(
|
|
52150
|
+
f,
|
|
52151
|
+
[
|
|
52152
|
+
"id",
|
|
52153
|
+
"name",
|
|
52154
|
+
"address",
|
|
52155
|
+
"addressBase",
|
|
52156
|
+
"dataType",
|
|
52157
|
+
"byteOrder",
|
|
52158
|
+
"wordOrder",
|
|
52159
|
+
"scale",
|
|
52160
|
+
"offset",
|
|
52161
|
+
"functionCode",
|
|
52162
|
+
...p.measurements.includes(f) ? ["unit", "kind", "pollIntervalMs", "rollover"] : ["onValue", "offValue"]
|
|
52163
|
+
],
|
|
52164
|
+
`${path}.${f.id}`
|
|
52165
|
+
);
|
|
52166
|
+
});
|
|
52167
|
+
p.measurements.forEach((m) => {
|
|
52168
|
+
if (!m || ![3, 4].includes(m.functionCode) || !["ampere", "volt", "watt", "watt-hour", "percent"].includes(m.unit) || !["live", "cumulative"].includes(m.kind) || !integer(m.pollIntervalMs, 100, 36e5))
|
|
52169
|
+
fail(path, "measurement requires FC03/04, physical unit, kind, poll interval 100..3600000ms");
|
|
52170
|
+
if (m?.rollover !== void 0 && (m.kind !== "cumulative" || !Number.isFinite(m.rollover) || m.rollover <= 0))
|
|
52171
|
+
fail(path, "rollover must be an explicit positive raw modulus on cumulative measurements");
|
|
52172
|
+
if (m?.kind === "cumulative" && m.scale <= 0) fail(path, "cumulative measurements require a positive scale");
|
|
52173
|
+
});
|
|
52174
|
+
p.actions.forEach((a) => {
|
|
52175
|
+
if (!a || ![5, 6, 16].includes(a.functionCode) || !Number.isFinite(a.onValue) || !Number.isFinite(a.offValue)) {
|
|
52176
|
+
fail(path, "action requires FC05/06/16 and finite on/off values");
|
|
52177
|
+
return;
|
|
52178
|
+
}
|
|
52179
|
+
if (a.functionCode === 5 && (a.dataType !== "uint16" || a.scale !== 1 || a.offset !== 0 || ![0, 1].includes(a.onValue) || ![0, 1].includes(a.offValue)))
|
|
52180
|
+
fail(path, "coil action requires identity uint16 and values 0 or 1");
|
|
52181
|
+
if (a.functionCode === 6 && registerCount(a) !== 1) fail(path, "FC06 requires a 16-bit dtype");
|
|
52182
|
+
for (const value2 of [a.onValue, a.offValue]) {
|
|
52183
|
+
const raw = (value2 - a.offset) / a.scale;
|
|
52184
|
+
const limits2 = {
|
|
52185
|
+
uint16: [0, 65535],
|
|
52186
|
+
int16: [-32768, 32767],
|
|
52187
|
+
uint32: [0, 4294967295],
|
|
52188
|
+
int32: [-2147483648, 2147483647],
|
|
52189
|
+
float32: [-34028234663852886e22, 34028234663852886e22]
|
|
52190
|
+
}[a.dataType];
|
|
52191
|
+
if (!limits2 || !Number.isFinite(raw) || raw < limits2[0] || raw > limits2[1] || a.dataType !== "float32" && !Number.isSafeInteger(raw))
|
|
52192
|
+
fail(path, "action on/off value cannot be represented by dtype and transform");
|
|
52193
|
+
}
|
|
52194
|
+
});
|
|
52195
|
+
});
|
|
52196
|
+
config.devices.forEach((d, i) => {
|
|
52197
|
+
keys(d, ["id", "name", "connectionId", "unitId", "profileId", "profileVersion"], `modbus.devices[${i}]`);
|
|
52198
|
+
if (!name(d.name) || !integer(d.unitId, 1, 247) || !config.connections.some((c) => c.id === d.connectionId) || !findProfile(config, d))
|
|
52199
|
+
fail(`modbus.devices[${i}]`, "device requires name, unit 1..247, existing connection and exact profile version");
|
|
52200
|
+
});
|
|
52201
|
+
return errors;
|
|
52202
|
+
}
|
|
52203
|
+
function validateModbusBindings(snapshot) {
|
|
52204
|
+
const errors = [];
|
|
52205
|
+
if (!Array.isArray(snapshot.physicalPoints)) return errors;
|
|
52206
|
+
const config = snapshot.modbus;
|
|
52207
|
+
const valid = config && validateModbus(config).length === 0;
|
|
52208
|
+
for (const [i, point] of snapshot.physicalPoints.entries()) {
|
|
52209
|
+
if (point?.modbus === void 0) {
|
|
52210
|
+
if (point?.hardwareProfile === "modbus")
|
|
52211
|
+
errors.push({
|
|
52212
|
+
path: `physicalPoints[${i}].modbus`,
|
|
52213
|
+
code: "invalid_modbus_binding",
|
|
52214
|
+
message: "Modbus points require a device binding"
|
|
52215
|
+
});
|
|
52216
|
+
continue;
|
|
52217
|
+
}
|
|
52218
|
+
const path = `physicalPoints[${i}].modbus`;
|
|
52219
|
+
const fail = (message) => errors.push({ path, code: "invalid_modbus_binding", message });
|
|
52220
|
+
if (!point.modbus || typeof point.modbus !== "object" || Array.isArray(point.modbus)) {
|
|
52221
|
+
fail("binding must be an object");
|
|
52222
|
+
continue;
|
|
52223
|
+
}
|
|
52224
|
+
const binding = point.modbus;
|
|
52225
|
+
if (!valid) {
|
|
52226
|
+
fail("valid Modbus configuration required");
|
|
52227
|
+
continue;
|
|
52228
|
+
}
|
|
52229
|
+
const device = config.devices.find((d) => d.id === binding.deviceId);
|
|
52230
|
+
const profile = device && findProfile(config, device);
|
|
52231
|
+
if (!profile || Object.keys(binding).some((key) => !["deviceId", "measurementId", "actionId"].includes(key)) || !binding.measurementId && !binding.actionId) {
|
|
52232
|
+
fail("existing device and named measurement/action required");
|
|
52233
|
+
continue;
|
|
52234
|
+
}
|
|
52235
|
+
const measurement = profile.measurements.find((m) => m.id === binding.measurementId);
|
|
52236
|
+
const action = profile.actions.find((a) => a.id === binding.actionId);
|
|
52237
|
+
if (binding.measurementId && !measurement) fail("unknown profile measurement");
|
|
52238
|
+
if (binding.actionId && !action) fail("unknown profile action (built-in meters are read-only)");
|
|
52239
|
+
if (Array.isArray(snapshot.logicalChannels))
|
|
52240
|
+
for (const channel of snapshot.logicalChannels) {
|
|
52241
|
+
if (channel?.physicalPointId !== point.id) continue;
|
|
52242
|
+
const capabilities = Array.isArray(channel.capabilities) ? channel.capabilities : [];
|
|
52243
|
+
if (capabilities.includes("output") && !action) fail("output requires named action");
|
|
52244
|
+
if (capabilities.includes("input") && !measurement) fail("input requires named measurement");
|
|
52245
|
+
if (capabilities.includes("measurement") && (!measurement || channel.measurement?.unit !== measurement.unit || (channel.measurement?.kind ?? "live") !== measurement.kind || channel.measurement?.scale !== 1 || channel.measurement?.offset !== 0))
|
|
52246
|
+
fail("measurement channel must match profile unit/kind with identity transform");
|
|
52247
|
+
}
|
|
52248
|
+
}
|
|
52249
|
+
return errors;
|
|
52250
|
+
}
|
|
52251
|
+
|
|
52252
|
+
// backend/configuration.ts
|
|
52253
|
+
var HARDWARE_PROFILES = ["751-9301", "879-3000", "879-1300", "modbus"];
|
|
52019
52254
|
var CHANNEL_PROFILES = [
|
|
52020
52255
|
"metered-switched-load",
|
|
52021
52256
|
"pulsed-lock-bank",
|
|
@@ -52089,7 +52324,9 @@ function validateSnapshot(snapshot) {
|
|
|
52089
52324
|
];
|
|
52090
52325
|
}
|
|
52091
52326
|
const errors = [];
|
|
52092
|
-
exactKeys(value, "$", ["version", "physicalPoints", "logicalChannels"], errors);
|
|
52327
|
+
exactKeys(value, "$", ["version", "physicalPoints", "logicalChannels", "modbus"], errors, ["modbus"]);
|
|
52328
|
+
if (value.modbus !== void 0) errors.push(...validateModbus(value.modbus));
|
|
52329
|
+
errors.push(...validateModbusBindings(value));
|
|
52093
52330
|
if (value.version !== CONFIGURATION_PROTOCOL_VERSION)
|
|
52094
52331
|
errors.push({
|
|
52095
52332
|
path: "version",
|
|
@@ -52104,7 +52341,7 @@ function validateSnapshot(snapshot) {
|
|
|
52104
52341
|
points.forEach((point, index) => {
|
|
52105
52342
|
const path = `physicalPoints[${index}]`;
|
|
52106
52343
|
if (!record(point, path, errors)) return;
|
|
52107
|
-
exactKeys(point, path, ["id", "hardwareProfile", "channel"], errors);
|
|
52344
|
+
exactKeys(point, path, ["id", "hardwareProfile", "channel", "modbus"], errors, ["modbus"]);
|
|
52108
52345
|
addId(point.id, `${path}.id`, pointIds, errors);
|
|
52109
52346
|
enumValue(point.hardwareProfile, `${path}.hardwareProfile`, HARDWARE_PROFILES, errors);
|
|
52110
52347
|
if (typeof point.channel !== "number" || !Number.isSafeInteger(point.channel) || point.channel < 0)
|
|
@@ -52348,7 +52585,7 @@ function validateProfile(value, capabilities, path, errors) {
|
|
|
52348
52585
|
);
|
|
52349
52586
|
}
|
|
52350
52587
|
function presetChannel(application) {
|
|
52351
|
-
const
|
|
52588
|
+
const base2 = {
|
|
52352
52589
|
id: application.channelId,
|
|
52353
52590
|
physicalPointId: application.physicalPointId,
|
|
52354
52591
|
profile: application.presetId,
|
|
@@ -52356,21 +52593,21 @@ function presetChannel(application) {
|
|
|
52356
52593
|
};
|
|
52357
52594
|
switch (application.presetId) {
|
|
52358
52595
|
case "metered-switched-load":
|
|
52359
|
-
return { ...
|
|
52596
|
+
return { ...base2, capabilities: ["output", "measurement"], measurement: { unit: "watt", scale: 1, offset: 0 } };
|
|
52360
52597
|
case "pulsed-lock-bank":
|
|
52361
|
-
return { ...
|
|
52598
|
+
return { ...base2, capabilities: ["output", "pulse"], pulse: { durationMs: 500 } };
|
|
52362
52599
|
case "guarded-enable-request":
|
|
52363
52600
|
if (!application.guardChannelId) throw new Error("guarded enable requests require a guard channel");
|
|
52364
52601
|
return {
|
|
52365
|
-
...
|
|
52602
|
+
...base2,
|
|
52366
52603
|
capabilities: ["output", "guard"],
|
|
52367
52604
|
guard: { channelId: application.guardChannelId, when: "on" }
|
|
52368
52605
|
};
|
|
52369
52606
|
case "generic-monitored-input":
|
|
52370
|
-
return { ...
|
|
52607
|
+
return { ...base2, capabilities: ["input"] };
|
|
52371
52608
|
case "generic-digital-output":
|
|
52372
52609
|
return {
|
|
52373
|
-
...
|
|
52610
|
+
...base2,
|
|
52374
52611
|
capabilities: application.feedbackChannelId ? ["output", "feedback"] : ["output"],
|
|
52375
52612
|
...application.feedbackChannelId ? { feedback: { channelId: application.feedbackChannelId, expected: "match", timeoutMs: 1e3 } } : {}
|
|
52376
52613
|
};
|
|
@@ -52997,10 +53234,10 @@ function canonicalEnvelope(data, category) {
|
|
|
52997
53234
|
}
|
|
52998
53235
|
function validEnvelope(data, now) {
|
|
52999
53236
|
const time = sourceTime(data.timestamp);
|
|
53000
|
-
return time !== null && time <= now && typeof data.streamId === "string" &&
|
|
53237
|
+
return time !== null && time <= now && typeof data.streamId === "string" && data.streamId.trim().length > 0 && data.streamId.length <= 128 && Number.isSafeInteger(data.sequence) && data.sequence >= 1;
|
|
53001
53238
|
}
|
|
53002
53239
|
function admitEnvelope(state, data, category, now) {
|
|
53003
|
-
const stream = data.streamId
|
|
53240
|
+
const stream = data.streamId;
|
|
53004
53241
|
const timestamp = sourceTime(data.timestamp);
|
|
53005
53242
|
const sequence = data.sequence;
|
|
53006
53243
|
if (state.retiredStreams.includes(stream) || state.trackingExhausted) return "rejected";
|
|
@@ -53166,7 +53403,7 @@ var WagoDiagnosticsStore = class {
|
|
|
53166
53403
|
const canonical = canonicalEnvelope(data, kind);
|
|
53167
53404
|
if (canonical && !validEnvelope(data, this.now())) return false;
|
|
53168
53405
|
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
53169
|
-
if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) ||
|
|
53406
|
+
if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) || typeof data.readiness.hardwareAvailable !== "boolean"))
|
|
53170
53407
|
return false;
|
|
53171
53408
|
if (canonical && kind === "state" && !(data.contentHash === null || typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash)))
|
|
53172
53409
|
return false;
|
|
@@ -59411,7 +59648,7 @@ function diagnosticReferences(nodes, channelIds, revision, capabilities) {
|
|
|
59411
59648
|
resourceId: node.resourceId,
|
|
59412
59649
|
channelId,
|
|
59413
59650
|
control,
|
|
59414
|
-
href: `/resources/${node.resourceId}/flows`,
|
|
59651
|
+
href: `/resources/${node.resourceId}/flows?node=${encodeURIComponent(node.id)}`,
|
|
59415
59652
|
invalid: !channelIds.includes(channelId) || control && (node.data.expectedConfigurationRevision !== revision || capabilities && (!own(capabilities, channelId)?.includes("output") || node.data.action === "pulse" && !own(capabilities, channelId)?.includes("pulse"))),
|
|
59416
59653
|
conflict: control && nodes.some(
|
|
59417
59654
|
(other) => other.type === "plugin.wago.command" && other.resourceId !== node.resourceId && other.data.channelId === channelId
|
|
@@ -59424,6 +59661,90 @@ var WagoDiagnosticsService = class {
|
|
|
59424
59661
|
__publicField(this, "context", context);
|
|
59425
59662
|
__publicField(this, "wago", wago);
|
|
59426
59663
|
}
|
|
59664
|
+
async getResource(resourceId) {
|
|
59665
|
+
const nodes = await this.context.dataSource.getRepository(import_plugins_backend_sdk15.ResourceFlowNode).createQueryBuilder("node").where("node.resourceId = :resourceId", { resourceId }).andWhere("node.type LIKE :type", { type: "plugin.wago.%" }).orderBy("node.id", "ASC").take(1001).getMany();
|
|
59666
|
+
const controllerIds = /* @__PURE__ */ new Set();
|
|
59667
|
+
let invalidControllerReferences = 0;
|
|
59668
|
+
for (const node of nodes.slice(0, 1e3)) {
|
|
59669
|
+
const id = node.data.controllerId;
|
|
59670
|
+
if (typeof id !== "number" || !Number.isSafeInteger(id) || id <= 0) {
|
|
59671
|
+
invalidControllerReferences++;
|
|
59672
|
+
} else {
|
|
59673
|
+
controllerIds.add(id);
|
|
59674
|
+
}
|
|
59675
|
+
}
|
|
59676
|
+
const selectedControllerIds = [...controllerIds].slice(0, 20);
|
|
59677
|
+
if (!selectedControllerIds.length)
|
|
59678
|
+
return { resourceId, controllers: [], invalidControllerReferences, truncated: nodes.length > 1e3 };
|
|
59679
|
+
const localNodes = nodes.slice(0, 1e3);
|
|
59680
|
+
const localChannelIds = [
|
|
59681
|
+
...new Set(
|
|
59682
|
+
localNodes.map((node) => node.data.channelId).filter((channelId) => typeof channelId === "string")
|
|
59683
|
+
)
|
|
59684
|
+
];
|
|
59685
|
+
const controllerIdsForQuery = selectedControllerIds.map(String);
|
|
59686
|
+
const [controllers, appliedRevisions, conflictNodes] = await Promise.all([
|
|
59687
|
+
this.context.getRepository(WagoController).createQueryBuilder("controller").select(["controller.id", "controller.name", "controller.hardwareId"]).where("controller.id IN (:...controllerIds)", { controllerIds: selectedControllerIds }).getMany(),
|
|
59688
|
+
this.context.getRepository(WagoConfigurationRevision).createQueryBuilder("revision").select(["revision.controllerId", "revision.revision", "revision.snapshot"]).distinctOn(["revision.controllerId"]).where("revision.controllerId IN (:...controllerIds)", { controllerIds: selectedControllerIds }).andWhere("revision.state = :state", { state: "applied" }).orderBy("revision.controllerId", "ASC").addOrderBy("revision.revision", "DESC").getMany(),
|
|
59689
|
+
localChannelIds.length ? this.context.dataSource.getRepository(import_plugins_backend_sdk15.ResourceFlowNode).createQueryBuilder("node").where("node.data ->> 'controllerId' IN (:...controllerIds)", { controllerIds: controllerIdsForQuery }).andWhere("node.data ->> 'channelId' IN (:...channelIds)", { channelIds: localChannelIds }).andWhere("node.type = :type", { type: "plugin.wago.command" }).take(1001).getMany() : Promise.resolve([])
|
|
59690
|
+
]);
|
|
59691
|
+
const controllersById = new Map(controllers.map((controller) => [controller.id, controller]));
|
|
59692
|
+
const appliedByControllerId = new Map(appliedRevisions.map((revision) => [revision.controllerId, revision]));
|
|
59693
|
+
const referencesTruncated = conflictNodes.length > 1e3;
|
|
59694
|
+
const conflictNodesByControllerId = /* @__PURE__ */ new Map();
|
|
59695
|
+
for (const node of conflictNodes.slice(0, 1e3)) {
|
|
59696
|
+
const controllerId = node.data.controllerId;
|
|
59697
|
+
if (typeof controllerId !== "number") continue;
|
|
59698
|
+
const matchingNodes = conflictNodesByControllerId.get(controllerId) ?? [];
|
|
59699
|
+
matchingNodes.push(node);
|
|
59700
|
+
conflictNodesByControllerId.set(controllerId, matchingNodes);
|
|
59701
|
+
}
|
|
59702
|
+
const controllersResult = selectedControllerIds.map((controllerId) => {
|
|
59703
|
+
const controller = controllersById.get(controllerId);
|
|
59704
|
+
if (!controller)
|
|
59705
|
+
return {
|
|
59706
|
+
controllerId,
|
|
59707
|
+
name: `Controller ${controllerId}`,
|
|
59708
|
+
unavailable: true,
|
|
59709
|
+
references: [],
|
|
59710
|
+
referencesTruncated: false
|
|
59711
|
+
};
|
|
59712
|
+
try {
|
|
59713
|
+
const applied = appliedByControllerId.get(controllerId);
|
|
59714
|
+
const appliedSnapshot = applied ? JSON.parse(applied.snapshot) : null;
|
|
59715
|
+
const controllerNodes = localNodes.filter((node) => node.data.controllerId === controllerId);
|
|
59716
|
+
const references = diagnosticReferences(
|
|
59717
|
+
[...controllerNodes, ...conflictNodesByControllerId.get(controllerId) ?? []],
|
|
59718
|
+
appliedSnapshot?.logicalChannels.map((channel) => channel.id) ?? [],
|
|
59719
|
+
applied?.revision ?? null,
|
|
59720
|
+
Object.fromEntries(
|
|
59721
|
+
appliedSnapshot?.logicalChannels.map((channel) => [channel.id, channel.capabilities]) ?? []
|
|
59722
|
+
)
|
|
59723
|
+
).filter((reference) => reference.resourceId === resourceId);
|
|
59724
|
+
return {
|
|
59725
|
+
controllerId,
|
|
59726
|
+
name: controller.name ?? controller.hardwareId,
|
|
59727
|
+
unavailable: false,
|
|
59728
|
+
references,
|
|
59729
|
+
referencesTruncated
|
|
59730
|
+
};
|
|
59731
|
+
} catch {
|
|
59732
|
+
return {
|
|
59733
|
+
controllerId,
|
|
59734
|
+
name: controller.name ?? controller.hardwareId,
|
|
59735
|
+
unavailable: true,
|
|
59736
|
+
references: [],
|
|
59737
|
+
referencesTruncated: false
|
|
59738
|
+
};
|
|
59739
|
+
}
|
|
59740
|
+
});
|
|
59741
|
+
return {
|
|
59742
|
+
resourceId,
|
|
59743
|
+
controllers: controllersResult,
|
|
59744
|
+
invalidControllerReferences,
|
|
59745
|
+
truncated: nodes.length > 1e3 || controllerIds.size > 20
|
|
59746
|
+
};
|
|
59747
|
+
}
|
|
59427
59748
|
async get(controllerId) {
|
|
59428
59749
|
const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
|
|
59429
59750
|
if (!controller) throw new import_common11.NotFoundException("WAGO controller not found");
|
|
@@ -59464,7 +59785,7 @@ var WagoDiagnosticsService = class {
|
|
|
59464
59785
|
stateConnected: runtime.connected ?? null,
|
|
59465
59786
|
stateHardwareAvailable: runtime.hardwareAvailable ?? null,
|
|
59466
59787
|
stateSourceAt: runtime.stateSourceAt ?? null,
|
|
59467
|
-
sequenceExplanation: runtime.activeStream ? "Gaps are scoped to boot
|
|
59788
|
+
sequenceExplanation: runtime.activeStream ? "Gaps are scoped to boot stream ID and message category; duplicates and retired streams are ignored." : "Legacy payloads have no source envelope; sequence gaps and source freshness are unavailable.",
|
|
59468
59789
|
configuration: {
|
|
59469
59790
|
draftUpdatedAt: draft?.updatedAt ?? null,
|
|
59470
59791
|
draftChanged: !!draft && (!latest || configurationHash(JSON.parse(draft.snapshot)) !== latest.contentHash || configurationHash(metadataFromProvenance(draft.presetProvenance)) !== configurationHash(metadataFromProvenance(latest.presetProvenance))),
|
|
@@ -59513,9 +59834,9 @@ var WagoDiagnosticsService = class {
|
|
|
59513
59834
|
limitations: [
|
|
59514
59835
|
"Hardware readiness remains unknown. Legacy samples have no source time and are never current.",
|
|
59515
59836
|
"Diagnostics retain at most 256 channels, 50 recent events and 15 minutes in this process.",
|
|
59516
|
-
"
|
|
59837
|
+
"Flow links select the affected node. Diagnostics warnings do not block resource usage; required flow nodes determine gating.",
|
|
59517
59838
|
"The MQTT SDK exposes neither retained-message flags nor broker connection events. Source age is bounded to 90 seconds; immediate broker-disconnect detection is unavailable.",
|
|
59518
|
-
"Stream tracking keeps at most 16 retired boot
|
|
59839
|
+
"Stream tracking keeps at most 16 retired boot stream IDs per controller and six category counters, and fails closed when exhausted. Replayed source times never become receipt times."
|
|
59519
59840
|
]
|
|
59520
59841
|
};
|
|
59521
59842
|
}
|
|
@@ -59531,10 +59852,17 @@ var WagoDiagnosticsController = class {
|
|
|
59531
59852
|
constructor(diagnostics) {
|
|
59532
59853
|
__publicField(this, "diagnostics", diagnostics);
|
|
59533
59854
|
}
|
|
59855
|
+
getResource(id) {
|
|
59856
|
+
return this.diagnostics.getResource(id);
|
|
59857
|
+
}
|
|
59534
59858
|
get(id) {
|
|
59535
59859
|
return this.diagnostics.get(id);
|
|
59536
59860
|
}
|
|
59537
59861
|
};
|
|
59862
|
+
__decorateClass([
|
|
59863
|
+
(0, import_common12.Get)("resources/:id/diagnostics"),
|
|
59864
|
+
__decorateParam(0, (0, import_common12.Param)("id", import_common12.ParseIntPipe))
|
|
59865
|
+
], WagoDiagnosticsController.prototype, "getResource", 1);
|
|
59538
59866
|
__decorateClass([
|
|
59539
59867
|
(0, import_common12.Get)("controllers/:id/diagnostics"),
|
|
59540
59868
|
__decorateParam(0, (0, import_common12.Param)("id", import_common12.ParseIntPipe))
|