@attraccess/plugin-wago 0.1.0-nightly.929.1 → 0.1.0-nightly.932.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
@@ -277,7 +277,36 @@ var CHANNEL_PROFILES = [
277
277
  "generic-digital-output",
278
278
  "generic-monitored-input"
279
279
  ];
280
- var CAPABILITIES = ["output", "input", "measurement", "pulse", "guard"];
280
+ var CAPABILITIES = ["output", "input", "measurement", "pulse", "guard", "feedback"];
281
+ var WAGO_PRESETS = [
282
+ {
283
+ id: "metered-switched-load",
284
+ name: "Metered switched load",
285
+ description: "Switches an output off immediately when disconnected and reports a transformed measurement."
286
+ },
287
+ {
288
+ id: "pulsed-lock-bank",
289
+ name: "Pulsed lock bank",
290
+ description: "Pulses an output briefly and returns it to off immediately when disconnected."
291
+ },
292
+ {
293
+ id: "guarded-enable-request",
294
+ name: "Guarded enable request",
295
+ description: "Makes a non-safety enable request only while an operational guard is satisfied."
296
+ },
297
+ { id: "generic-digital-output", name: "Generic digital output", description: "A conservative output foundation." },
298
+ { id: "generic-monitored-input", name: "Generic monitored input", description: "A monitored digital input foundation." }
299
+ ];
300
+ function applyPreset(snapshot, application) {
301
+ const preset = application && WAGO_PRESETS.find((item) => item.id === application.presetId);
302
+ if (!preset) throw new Error("unknown WAGO preset");
303
+ const channel = presetChannel(application);
304
+ const existingIndex = snapshot.logicalChannels.findIndex((item) => item.id === application.channelId);
305
+ const logicalChannels = [...snapshot.logicalChannels];
306
+ if (existingIndex === -1) logicalChannels.push(channel);
307
+ else logicalChannels[existingIndex] = channel;
308
+ return { ...snapshot, logicalChannels };
309
+ }
281
310
  function canonicalSnapshot(snapshot) {
282
311
  if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
283
312
  throw new Error("configuration snapshot must be an object");
@@ -321,6 +350,7 @@ function validateSnapshot(snapshot) {
321
350
  const channels = collection(value.logicalChannels, "logicalChannels", errors);
322
351
  const pointIds = /* @__PURE__ */ new Set();
323
352
  const channelIds = /* @__PURE__ */ new Set();
353
+ const channelsById = /* @__PURE__ */ new Map();
324
354
  points.forEach((point, index) => {
325
355
  const path = `physicalPoints[${index}]`;
326
356
  if (!record(point, path, errors)) return;
@@ -335,8 +365,11 @@ function validateSnapshot(snapshot) {
335
365
  });
336
366
  });
337
367
  channels.forEach((channel, index) => {
338
- if (channel && typeof channel === "object" && !Array.isArray(channel))
339
- addId(channel.id, `logicalChannels[${index}].id`, channelIds, errors);
368
+ if (channel && typeof channel === "object" && !Array.isArray(channel)) {
369
+ const item = channel;
370
+ addId(item.id, `logicalChannels[${index}].id`, channelIds, errors);
371
+ if (typeof item.id === "string") channelsById.set(item.id, item);
372
+ }
340
373
  });
341
374
  channels.forEach((channel, index) => {
342
375
  const path = `logicalChannels[${index}]`;
@@ -353,6 +386,7 @@ function validateSnapshot(snapshot) {
353
386
  "range",
354
387
  "pulse",
355
388
  "guard",
389
+ "feedback",
356
390
  "measurement"
357
391
  ],
358
392
  errors
@@ -365,6 +399,7 @@ function validateSnapshot(snapshot) {
365
399
  validateRange(channel.range, `${path}.range`, capabilities, errors);
366
400
  validatePulse(channel.pulse, `${path}.pulse`, capabilities, errors);
367
401
  validateGuard(channel.guard, `${path}.guard`, capabilities, channelIds, errors);
402
+ validateFeedback(channel.feedback, `${path}.feedback`, capabilities, channel.id, channelsById, errors);
368
403
  validateMeasurement(channel.measurement, `${path}.measurement`, capabilities, errors);
369
404
  validateProfile(channel.profile, capabilities, path, errors);
370
405
  });
@@ -411,7 +446,7 @@ function exactKeys(value, path, allowed, errors, optional = []) {
411
446
  message: "field is not supported by configuration version 1"
412
447
  })
413
448
  );
414
- allowed.filter((key) => !["range", "pulse", "guard", "measurement", ...optional].includes(key) && !(key in value)).forEach(
449
+ allowed.filter((key) => !["range", "pulse", "guard", "feedback", "measurement", ...optional].includes(key) && !(key in value)).forEach(
415
450
  (key) => errors.push({
416
451
  path: path === "$" ? key : `${path}.${key}`,
417
452
  code: "required_field",
@@ -519,6 +554,22 @@ function validateMeasurement(value, path, capabilities, errors) {
519
554
  if (!capabilities.has("measurement"))
520
555
  errors.push({ path, code: "unsupported_field", message: "measurement requires measurement capability" });
521
556
  }
557
+ function validateFeedback(value, path, capabilities, currentChannelId, channelsById, errors) {
558
+ if (value === void 0) return;
559
+ if (!record(value, path, errors)) return;
560
+ exactKeys(value, path, ["channelId", "expected", "timeoutMs"], errors);
561
+ const feedbackChannel = typeof value.channelId === "string" ? channelsById.get(value.channelId) : void 0;
562
+ if (!feedbackChannel)
563
+ errors.push(referenceError(`${path}.channelId`, "logical channel", value.channelId));
564
+ else if (value.channelId === currentChannelId || !Array.isArray(feedbackChannel.capabilities) || !feedbackChannel.capabilities.includes("input"))
565
+ errors.push({ path: `${path}.channelId`, code: "invalid_feedback_channel", message: "feedback must reference a distinct input channel" });
566
+ if (!["match", "inverse"].includes(value.expected))
567
+ errors.push({ path: `${path}.expected`, code: "unsupported_value", message: "expected must be match or inverse" });
568
+ if (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs <= 0)
569
+ errors.push({ path: `${path}.timeoutMs`, code: "invalid_timeout", message: "timeoutMs must be a positive integer" });
570
+ if (!capabilities.has("feedback"))
571
+ errors.push({ path, code: "unsupported_field", message: "feedback requires feedback capability" });
572
+ }
522
573
  function validateProfile(value, capabilities, path, errors) {
523
574
  const required = {
524
575
  "metered-switched-load": ["output", "measurement"],
@@ -535,6 +586,35 @@ function validateProfile(value, capabilities, path, errors) {
535
586
  })
536
587
  );
537
588
  }
589
+ function presetChannel(application) {
590
+ const base = {
591
+ id: application.channelId,
592
+ physicalPointId: application.physicalPointId,
593
+ profile: application.presetId,
594
+ disconnectPolicy: { mode: application.presetId === "generic-monitored-input" ? "hold" : "immediate" }
595
+ };
596
+ switch (application.presetId) {
597
+ case "metered-switched-load":
598
+ return { ...base, capabilities: ["output", "measurement"], measurement: { unit: "watt", scale: 1, offset: 0 } };
599
+ case "pulsed-lock-bank":
600
+ return { ...base, capabilities: ["output", "pulse"], pulse: { durationMs: 500 } };
601
+ case "guarded-enable-request":
602
+ if (!application.guardChannelId) throw new Error("guarded enable requests require a guard channel");
603
+ return {
604
+ ...base,
605
+ capabilities: ["output", "guard"],
606
+ guard: { channelId: application.guardChannelId, when: "on" }
607
+ };
608
+ case "generic-monitored-input":
609
+ return { ...base, capabilities: ["input"] };
610
+ case "generic-digital-output":
611
+ return {
612
+ ...base,
613
+ capabilities: application.feedbackChannelId ? ["output", "feedback"] : ["output"],
614
+ ...application.feedbackChannelId ? { feedback: { channelId: application.feedbackChannelId, expected: "match", timeoutMs: 1e3 } } : {}
615
+ };
616
+ }
617
+ }
538
618
  function referenceError(path, type, id) {
539
619
  return { path, code: "missing_reference", message: `${type} ${String(id)} does not exist in this snapshot` };
540
620
  }
@@ -554,6 +634,7 @@ var WagoConfigurationDraft = class {
554
634
  __publicField(this, "controllerId");
555
635
  __publicField(this, "snapshot");
556
636
  __publicField(this, "reviewedHash");
637
+ __publicField(this, "presetProvenance");
557
638
  __publicField(this, "updatedAt");
558
639
  }
559
640
  };
@@ -569,6 +650,9 @@ __decorateClass([
569
650
  __decorateClass([
570
651
  (0, import_plugins_backend_sdk4.Column)({ type: "varchar", name: "reviewed_hash", nullable: true })
571
652
  ], WagoConfigurationDraft.prototype, "reviewedHash", 2);
653
+ __decorateClass([
654
+ (0, import_plugins_backend_sdk4.Column)({ type: "text", name: "preset_provenance", nullable: true })
655
+ ], WagoConfigurationDraft.prototype, "presetProvenance", 2);
572
656
  __decorateClass([
573
657
  (0, import_plugins_backend_sdk4.Column)({ type: "varchar", name: "updated_at" })
574
658
  ], WagoConfigurationDraft.prototype, "updatedAt", 2);
@@ -645,13 +729,13 @@ var WagoService = class {
645
729
  __publicField(this, "subscriptionRetryTimer", null);
646
730
  __publicField(this, "activeSubscriptionGeneration", 0);
647
731
  __publicField(this, "destroyed", false);
648
- this.controllers = context.getRepository(WagoController);
649
- this.settings = context.getRepository(WagoSettings);
650
- this.enrollments = context.getRepository(WagoEnrollment);
651
- this.drafts = context.getRepository(WagoConfigurationDraft);
652
- this.revisions = context.getRepository(WagoConfigurationRevision);
653
732
  }
654
733
  async onModuleInit() {
734
+ this.controllers = this.context.getRepository(WagoController);
735
+ this.settings = this.context.getRepository(WagoSettings);
736
+ this.enrollments = this.context.getRepository(WagoEnrollment);
737
+ this.drafts = this.context.getRepository(WagoConfigurationDraft);
738
+ this.revisions = this.context.getRepository(WagoConfigurationRevision);
655
739
  const enrollments = await this.enrollments.createQueryBuilder("enrollment").where("enrollment.consumedAt IS NULL").getMany();
656
740
  for (const enrollment of enrollments) this.scheduleEnrollmentExpiry(enrollment);
657
741
  await this.subscribeConfiguredServers();
@@ -715,6 +799,12 @@ var WagoService = class {
715
799
  async setDefaultMqttServer(serverId) {
716
800
  return this.setSettings(serverId);
717
801
  }
802
+ async enrollmentCredentialSupport(mqttServerId) {
803
+ if (!await this.context.getMqttServerConfig(mqttServerId))
804
+ throw new import_common.NotFoundException(`MQTT server ${mqttServerId} not found`);
805
+ const providers = await this.context.getMqttCredentialProvisioning().availableProviders(mqttServerId);
806
+ return { automatic: providers.length > 0 };
807
+ }
718
808
  async getDraft(controllerId) {
719
809
  await this.claimedController(controllerId);
720
810
  return this.drafts.findOneBy({ controllerId });
@@ -728,6 +818,35 @@ var WagoService = class {
728
818
  const errors = validateSnapshot(JSON.parse(draft.snapshot));
729
819
  return { valid: errors.length === 0, errors };
730
820
  }
821
+ presets() {
822
+ return WAGO_PRESETS;
823
+ }
824
+ async previewPreset(controllerId, application) {
825
+ const draft = await this.draftForPreset(controllerId);
826
+ const snapshot = JSON.parse(draft.snapshot);
827
+ return { draftHash: configurationHash(snapshot), diff: configurationDiff(snapshot, applyPreset(snapshot, application)) };
828
+ }
829
+ async applyPreset(controllerId, application, selectedPaths, previewedDraftHash) {
830
+ return this.withConfigurationLock(controllerId, async () => {
831
+ const draft = await this.draftForPreset(controllerId);
832
+ const snapshot = JSON.parse(draft.snapshot);
833
+ if (previewedDraftHash !== configurationHash(snapshot))
834
+ throw new import_common.ConflictException("selected preset changes no longer match the configuration draft");
835
+ const candidate = applyPreset(snapshot, application);
836
+ const diff = configurationDiff(snapshot, candidate);
837
+ const validPaths = new Set(diff.map((change) => change.path));
838
+ if (!Array.isArray(selectedPaths) || selectedPaths.some((path) => !validPaths.has(path)))
839
+ throw new import_common.ConflictException("selected preset changes no longer match the configuration draft");
840
+ draft.snapshot = canonicalSnapshot(applySelectedChanges(snapshot, diff, selectedPaths));
841
+ draft.reviewedHash = null;
842
+ draft.presetProvenance = JSON.stringify([
843
+ ...parsePresetProvenance(draft.presetProvenance).slice(-99),
844
+ { presetId: application.presetId, appliedAt: (/* @__PURE__ */ new Date()).toISOString(), selectedPaths }
845
+ ]);
846
+ draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
847
+ return this.drafts.save(draft);
848
+ });
849
+ }
731
850
  async revisionsFor(controllerId, offset = 0, limit = 20) {
732
851
  await this.claimedController(controllerId);
733
852
  const pageOffset = Number.isSafeInteger(offset) && offset > 0 ? offset : 0;
@@ -780,7 +899,7 @@ var WagoService = class {
780
899
  await this.claimedController(controllerId);
781
900
  const serialized = canonicalSnapshot(snapshot);
782
901
  const existing = await this.drafts.findOneBy({ controllerId });
783
- const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, updatedAt: "" });
902
+ const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, presetProvenance: null, updatedAt: "" });
784
903
  draft.snapshot = serialized;
785
904
  draft.reviewedHash = null;
786
905
  draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -796,6 +915,12 @@ var WagoService = class {
796
915
  const diff = configurationDiff(previous ? JSON.parse(previous.snapshot) : null, JSON.parse(draft.snapshot));
797
916
  return { draft, previous, changed: diff.length > 0, diff };
798
917
  }
918
+ async draftForPreset(controllerId) {
919
+ await this.claimedController(controllerId);
920
+ const draft = await this.drafts.findOneBy({ controllerId });
921
+ if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
922
+ return draft;
923
+ }
799
924
  async publishDraftWhileLocked(controllerId) {
800
925
  const controller = await this.claimedController(controllerId);
801
926
  const draft = await this.drafts.findOneBy({ controllerId });
@@ -1414,6 +1539,36 @@ function safeEqual(left, right) {
1414
1539
  function isValidHardwareId(hardwareId) {
1415
1540
  return Boolean(hardwareId) && !/[/+#]/.test(hardwareId);
1416
1541
  }
1542
+ function parsePresetProvenance(value) {
1543
+ if (!value) return [];
1544
+ try {
1545
+ const parsed = JSON.parse(value);
1546
+ return Array.isArray(parsed) ? parsed : [];
1547
+ } catch {
1548
+ return [];
1549
+ }
1550
+ }
1551
+ function applySelectedChanges(snapshot, diff, selectedPaths) {
1552
+ const merged = JSON.parse(JSON.stringify(snapshot));
1553
+ const changes = new Map(diff.map((change) => [change.path, change]));
1554
+ for (const path of selectedPaths) {
1555
+ const change = changes.get(path);
1556
+ if (!change) continue;
1557
+ const segments = [...path.matchAll(/\.([^.[\]]+)|\[(\d+)\]/g)].map((match) => match[1] ?? Number(match[2]));
1558
+ if (!segments.length) continue;
1559
+ let target = merged;
1560
+ for (const segment of segments.slice(0, -1)) {
1561
+ const next = target[segment];
1562
+ if (next === void 0) target[segment] = typeof segment === "number" ? [] : {};
1563
+ target = target[segment];
1564
+ }
1565
+ const last = segments[segments.length - 1];
1566
+ if (last === void 0) continue;
1567
+ if (change.current === void 0) delete target[last];
1568
+ else target[last] = change.current;
1569
+ }
1570
+ return merged;
1571
+ }
1417
1572
 
1418
1573
  // backend/wago.controller.ts
1419
1574
  var WagoControllerApi = class {
@@ -1433,12 +1588,26 @@ var WagoControllerApi = class {
1433
1588
  const manualCredentials = body?.manualUsername || body?.manualPassword ? { username: body.manualUsername ?? "", password: body.manualPassword ?? "" } : void 0;
1434
1589
  return this.wago.createEnrollment(body?.hardwareId ?? "", body?.mqttServerId, manualCredentials);
1435
1590
  }
1591
+ credentialSupport(mqttServerId) {
1592
+ return this.wago.enrollmentCredentialSupport(mqttServerId);
1593
+ }
1436
1594
  claim(id, body) {
1437
1595
  return this.wago.claim(id, body?.name ?? "", body?.verifier ?? "", body?.mqttServerId);
1438
1596
  }
1439
1597
  draft(id) {
1440
1598
  return this.wago.getDraft(id);
1441
1599
  }
1600
+ presets() {
1601
+ return this.wago.presets();
1602
+ }
1603
+ previewPreset(id, body) {
1604
+ if (!body?.application) throw new import_common2.BadRequestException("application is required");
1605
+ return this.wago.previewPreset(id, body.application);
1606
+ }
1607
+ applyPreset(id, body) {
1608
+ if (!body?.application) throw new import_common2.BadRequestException("application is required");
1609
+ return this.wago.applyPreset(id, body.application, body.selectedPaths ?? [], body.previewedDraftHash ?? "");
1610
+ }
1442
1611
  saveDraft(id, body) {
1443
1612
  return this.wago.saveDraft(id, body?.snapshot);
1444
1613
  }
@@ -1475,6 +1644,10 @@ __decorateClass([
1475
1644
  (0, import_common2.Post)("enrollments"),
1476
1645
  __decorateParam(0, (0, import_common2.Body)())
1477
1646
  ], WagoControllerApi.prototype, "enrollment", 1);
1647
+ __decorateClass([
1648
+ (0, import_common2.Get)("enrollments/credential-support/:mqttServerId"),
1649
+ __decorateParam(0, (0, import_common2.Param)("mqttServerId", import_common2.ParseIntPipe))
1650
+ ], WagoControllerApi.prototype, "credentialSupport", 1);
1478
1651
  __decorateClass([
1479
1652
  (0, import_common2.Post)("controllers/:id/claim"),
1480
1653
  __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
@@ -1484,6 +1657,19 @@ __decorateClass([
1484
1657
  (0, import_common2.Get)("controllers/:id/configuration/draft"),
1485
1658
  __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
1486
1659
  ], WagoControllerApi.prototype, "draft", 1);
1660
+ __decorateClass([
1661
+ (0, import_common2.Get)("configuration/presets")
1662
+ ], WagoControllerApi.prototype, "presets", 1);
1663
+ __decorateClass([
1664
+ (0, import_common2.Post)("controllers/:id/configuration/presets/preview"),
1665
+ __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
1666
+ __decorateParam(1, (0, import_common2.Body)())
1667
+ ], WagoControllerApi.prototype, "previewPreset", 1);
1668
+ __decorateClass([
1669
+ (0, import_common2.Post)("controllers/:id/configuration/presets/apply"),
1670
+ __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
1671
+ __decorateParam(1, (0, import_common2.Body)())
1672
+ ], WagoControllerApi.prototype, "applyPreset", 1);
1487
1673
  __decorateClass([
1488
1674
  (0, import_common2.Post)("controllers/:id/configuration/draft"),
1489
1675
  __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
@@ -23,6 +23,7 @@ var migrations_exports = {};
23
23
  __export(migrations_exports, {
24
24
  AddWagoConfigurationRevisions1780000000002: () => AddWagoConfigurationRevisions1780000000002,
25
25
  AddWagoEnrollmentRevocation1780000000001: () => AddWagoEnrollmentRevocation1780000000001,
26
+ AddWagoPresetProvenance1780000000003: () => AddWagoPresetProvenance1780000000003,
26
27
  CreateWagoControllers1780000000000: () => CreateWagoControllers1780000000000
27
28
  });
28
29
  module.exports = __toCommonJS(migrations_exports);
@@ -98,9 +99,23 @@ var AddWagoConfigurationRevisions1780000000002 = class {
98
99
  await queryRunner.query('ALTER TABLE "plugin_wago_settings" DROP COLUMN "operational_prefix"');
99
100
  }
100
101
  };
102
+
103
+ // backend/migrations/1780000000003-add-wago-preset-provenance.ts
104
+ var AddWagoPresetProvenance1780000000003 = class {
105
+ constructor() {
106
+ __publicField(this, "name", "AddWagoPresetProvenance1780000000003");
107
+ }
108
+ async up(queryRunner) {
109
+ await queryRunner.query('ALTER TABLE "plugin_wago_configuration_drafts" ADD COLUMN "preset_provenance" text');
110
+ }
111
+ async down(queryRunner) {
112
+ await queryRunner.query('ALTER TABLE "plugin_wago_configuration_drafts" DROP COLUMN "preset_provenance"');
113
+ }
114
+ };
101
115
  // Annotate the CommonJS export names for ESM import in node:
102
116
  0 && (module.exports = {
103
117
  AddWagoConfigurationRevisions1780000000002,
104
118
  AddWagoEnrollmentRevocation1780000000001,
119
+ AddWagoPresetProvenance1780000000003,
105
120
  CreateWagoControllers1780000000000
106
121
  });