@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
- var HARDWARE_PROFILES = ["751-9301", "879-3000", "879-1300"];
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 base = {
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 { ...base, capabilities: ["output", "measurement"], measurement: { unit: "watt", scale: 1, offset: 0 } };
52596
+ return { ...base2, capabilities: ["output", "measurement"], measurement: { unit: "watt", scale: 1, offset: 0 } };
52360
52597
  case "pulsed-lock-bank":
52361
- return { ...base, capabilities: ["output", "pulse"], pulse: { durationMs: 500 } };
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
- ...base,
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 { ...base, capabilities: ["input"] };
52607
+ return { ...base2, capabilities: ["input"] };
52371
52608
  case "generic-digital-output":
52372
52609
  return {
52373
- ...base,
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" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(data.streamId) && Number.isSafeInteger(data.sequence) && data.sequence >= 1;
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.toLowerCase();
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) || data.readiness.hardwareAvailable !== void 0 && typeof data.readiness.hardwareAvailable !== "boolean"))
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 UUID and message category; duplicates and retired streams are ignored." : "Legacy payloads have no source envelope; sequence gaps and source freshness are unavailable.",
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
- "Resource-page slots are not integrated. Flow links open the affected resource flow; node IDs identify the exact nodes.",
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 UUIDs per controller and six category counters, and fails closed when exhausted. Replayed source times never become receipt times."
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))