@attraccess/plugin-wago 0.1.0-nightly.990.1 → 0.1.0-nightly.991.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
@@ -52544,14 +52544,7 @@ function parseAnnouncement(payload) {
52544
52544
  return { ...heartbeat, pairingCode: input.pairingCode.trim() };
52545
52545
  }
52546
52546
  function parseHeartbeat(payload) {
52547
- let value;
52548
- try {
52549
- value = JSON.parse(payload.toString("utf8"));
52550
- } catch {
52551
- throw new Error("announcement is not valid JSON");
52552
- }
52553
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("announcement must be an object");
52554
- const input = value;
52547
+ const input = parseObject(payload, "announcement");
52555
52548
  const required = ["hardwareId", "protocolVersion", "runtimeVersion"];
52556
52549
  for (const key of required)
52557
52550
  if (typeof input[key] !== "string" || !input[key].trim()) throw new Error(`announcement ${key} is required`);
@@ -52612,23 +52605,7 @@ function parseOperationalMessage(prefix, topic, payload) {
52612
52605
  throw new Error("operational streamId is invalid");
52613
52606
  const streamId = value.streamId;
52614
52607
  if (suffix === "state") {
52615
- if (typeof value.connected !== "boolean" || !isNullableInteger(value.revision) || !isNullableString(value.contentHash) || !isBooleanRecord(value.outputs) || value.inputs !== void 0 && !isBooleanRecord(value.inputs) || value.readiness !== void 0 && (!value.readiness || typeof value.readiness !== "object" || Array.isArray(value.readiness) || typeof value.readiness.hardwareAvailable !== "boolean"))
52616
- throw new Error("invalid state message");
52617
- return {
52618
- hardwareId,
52619
- message: {
52620
- category: "state",
52621
- timestamp,
52622
- streamId,
52623
- sequence,
52624
- connected: value.connected,
52625
- revision: value.revision,
52626
- contentHash: value.contentHash,
52627
- outputs: value.outputs,
52628
- ...value.inputs !== void 0 ? { inputs: value.inputs } : {},
52629
- ...value.readiness !== void 0 ? { readiness: { hardwareAvailable: value.readiness.hardwareAvailable } } : {}
52630
- }
52631
- };
52608
+ return { hardwareId, message: parseStateMessage(value, { timestamp, streamId, sequence }) };
52632
52609
  }
52633
52610
  if (suffix === "measurements") {
52634
52611
  return {
@@ -52673,6 +52650,20 @@ function parseOperationalMessage(prefix, topic, payload) {
52673
52650
  }
52674
52651
  };
52675
52652
  }
52653
+ function parseStateMessage(value, envelope) {
52654
+ if (typeof value.connected !== "boolean" || !isNullableInteger(value.revision) || !isNullableString(value.contentHash) || !isBooleanRecord(value.outputs) || value.inputs !== void 0 && !isBooleanRecord(value.inputs) || value.readiness !== void 0 && (!value.readiness || typeof value.readiness !== "object" || Array.isArray(value.readiness) || typeof value.readiness.hardwareAvailable !== "boolean"))
52655
+ throw new Error("invalid state message");
52656
+ return {
52657
+ category: "state",
52658
+ ...envelope,
52659
+ connected: value.connected,
52660
+ revision: value.revision,
52661
+ contentHash: value.contentHash,
52662
+ outputs: value.outputs,
52663
+ ...value.inputs !== void 0 ? { inputs: value.inputs } : {},
52664
+ ...value.readiness !== void 0 ? { readiness: { hardwareAvailable: value.readiness.hardwareAvailable } } : {}
52665
+ };
52666
+ }
52676
52667
  function configurationReportedHardwareId(prefix, topic) {
52677
52668
  const topicPrefix = `${normalizeOperationalPrefix(prefix)}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/`;
52678
52669
  const topicSuffix = "/configuration/reported";
@@ -52973,30 +52964,33 @@ function validateModbusBindings(snapshot) {
52973
52964
  if (Array.isArray(snapshot.logicalChannels))
52974
52965
  for (const channel of snapshot.logicalChannels) {
52975
52966
  if (channel?.physicalPointId !== point.id) continue;
52976
- const capabilities = Array.isArray(channel.capabilities) ? channel.capabilities : [];
52977
- if (capabilities.includes("output") && action && device) {
52978
- for (let offset = 0; offset < registerCount(action); offset++) {
52979
- const key = JSON.stringify([
52980
- device.connectionId,
52981
- device.unitId,
52982
- action.functionCode === 5 ? "coil" : "register",
52983
- wireAddress(action) + offset
52984
- ]);
52985
- if (outputOwners.has(key)) fail("each physical Modbus output must have a single logical owner");
52986
- outputOwners.add(key);
52987
- }
52988
- }
52989
- if (capabilities.includes("input") && !capabilities.includes("measurement"))
52990
- fail(
52991
- measurement ? "Modbus register inputs require measurement capability and its named measurement transform" : "input requires named measurement"
52992
- );
52993
- if (capabilities.includes("output") && !action) fail("output requires named action");
52994
- if (capabilities.includes("measurement") && (!measurement || channel.measurement?.unit !== measurement.unit || (channel.measurement?.kind ?? "live") !== measurement.kind || channel.measurement?.scale !== 1 || channel.measurement?.offset !== 0))
52995
- fail("measurement channel must match profile unit/kind with identity transform");
52967
+ validateChannelBinding(channel, measurement, action, device, outputOwners, fail);
52996
52968
  }
52997
52969
  }
52998
52970
  return errors;
52999
52971
  }
52972
+ function validateChannelBinding(channel, measurement, action, device, outputOwners, fail) {
52973
+ const capabilities = Array.isArray(channel.capabilities) ? channel.capabilities : [];
52974
+ if (capabilities.includes("output") && action && device) {
52975
+ for (let offset = 0; offset < registerCount(action); offset++) {
52976
+ const key = JSON.stringify([
52977
+ device.connectionId,
52978
+ device.unitId,
52979
+ action.functionCode === 5 ? "coil" : "register",
52980
+ wireAddress(action) + offset
52981
+ ]);
52982
+ if (outputOwners.has(key)) fail("each physical Modbus output must have a single logical owner");
52983
+ outputOwners.add(key);
52984
+ }
52985
+ }
52986
+ if (capabilities.includes("input") && !capabilities.includes("measurement"))
52987
+ fail(
52988
+ measurement ? "Modbus register inputs require measurement capability and its named measurement transform" : "input requires named measurement"
52989
+ );
52990
+ if (capabilities.includes("output") && !action) fail("output requires named action");
52991
+ if (capabilities.includes("measurement") && (!measurement || channel.measurement?.unit !== measurement.unit || (channel.measurement?.kind ?? "live") !== measurement.kind || channel.measurement?.scale !== 1 || channel.measurement?.offset !== 0))
52992
+ fail("measurement channel must match profile unit/kind with identity transform");
52993
+ }
53000
52994
 
53001
52995
  // backend/configuration.ts
53002
52996
  var HARDWARE_PROFILES = ["751-9301", "879-3000", "879-1300", "modbus"];
@@ -53897,13 +53891,7 @@ var WagoCommandHandler = class {
53897
53891
  __publicField(this, "dependencies", dependencies);
53898
53892
  __publicField(this, "pending", /* @__PURE__ */ new Map());
53899
53893
  }
53900
- async schema(config, resourceId) {
53901
- const controllers = await this.dependencies.controllers().find({ where: { trustState: "claimed" }, order: { name: "ASC" } });
53902
- const controllerId = positiveInteger2(config.controllerId);
53903
- const revision2 = controllerId ? await this.dependencies.appliedRevision(controllerId) : null;
53904
- const snapshot = revision2 ? JSON.parse(revision2.snapshot) : null;
53905
- const channelId2 = typeof config.channelId === "string" ? config.channelId : void 0;
53906
- const outputChannels = snapshot?.logicalChannels.filter((item) => item.capabilities.includes("output")) ?? [];
53894
+ async channelNames(controllerId, revision2) {
53907
53895
  let names = {};
53908
53896
  if (controllerId) {
53909
53897
  const draft = await this.dependencies.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId });
@@ -53913,6 +53901,16 @@ var WagoCommandHandler = class {
53913
53901
  } catch {
53914
53902
  }
53915
53903
  }
53904
+ return names;
53905
+ }
53906
+ async schema(config, resourceId) {
53907
+ const controllers = await this.dependencies.controllers().find({ where: { trustState: "claimed" }, order: { name: "ASC" } });
53908
+ const controllerId = positiveInteger2(config.controllerId);
53909
+ const revision2 = controllerId ? await this.dependencies.appliedRevision(controllerId) : null;
53910
+ const snapshot = revision2 ? JSON.parse(revision2.snapshot) : null;
53911
+ const channelId2 = typeof config.channelId === "string" ? config.channelId : void 0;
53912
+ const outputChannels = snapshot?.logicalChannels.filter((item) => item.capabilities.includes("output")) ?? [];
53913
+ const names = await this.channelNames(controllerId, revision2);
53916
53914
  const channel = outputChannels.find((item) => item.id === channelId2);
53917
53915
  const references = channelId2 && controllerId ? await this.references(controllerId, channelId2, resourceId) : [];
53918
53916
  const properties = {
@@ -54345,6 +54343,30 @@ var faultCodes = /* @__PURE__ */ new Set([
54345
54343
  function identifier(value) {
54346
54344
  return typeof value === "string" && value.trim().length > 0 && value.length <= 256 && !Array.from(value).some((character) => character.charCodeAt(0) < 32);
54347
54345
  }
54346
+ function isObject(value) {
54347
+ return !!value && typeof value === "object" && !Array.isArray(value);
54348
+ }
54349
+ function validStatePayload(data, kind, canonical) {
54350
+ if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) || data.readiness.hardwareAvailable !== void 0 && typeof data.readiness.hardwareAvailable !== "boolean"))
54351
+ return false;
54352
+ if (canonical && kind === "state" && !(data.contentHash === null || typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash)))
54353
+ return false;
54354
+ if (canonical && kind === "state" && (typeof data.connected !== "boolean" || !(data.revision === null || Number.isSafeInteger(data.revision) && data.revision >= 0) || !isObject(data.outputs) || data.inputs !== void 0 && !isObject(data.inputs) || ![...Object.values(data.outputs), ...Object.values(isObject(data.inputs) ? data.inputs : {})].every(
54355
+ (value) => typeof value === "boolean"
54356
+ )))
54357
+ return false;
54358
+ return true;
54359
+ }
54360
+ function validEventPayload(data, kind, canonical) {
54361
+ if (canonical && kind === "configuration/reported" && (!Number.isSafeInteger(data.revision) || data.revision < 1 || typeof data.contentHash !== "string" || !/^[0-9a-f]{64}$/i.test(data.contentHash) || !Array.isArray(data.errors)))
54362
+ return false;
54363
+ if (kind === "measurements" && (!identifier(data.channelId) || typeof data.value !== "number" || !Number.isFinite(data.value) || (canonical ? !Number.isSafeInteger(data.value) || !CANONICAL_UNITS.includes(data.unit) || !["live", "cumulative"].includes(data.kind) : !["ampere", "volt", "watt", "percent"].includes(data.unit))))
54364
+ return false;
54365
+ if (kind === "faults" && !identifier(data.channelId)) return false;
54366
+ if (kind === "acknowledgements" && (!identifier(data.id) || !["accepted", "duplicate", "rejected"].includes(data.status)))
54367
+ return false;
54368
+ return true;
54369
+ }
54348
54370
  var WagoDiagnosticsStore = class {
54349
54371
  constructor(now = Date.now) {
54350
54372
  __publicField(this, "now", now);
@@ -54436,27 +54458,40 @@ var WagoDiagnosticsStore = class {
54436
54458
  if (!DIAGNOSTIC_CATEGORIES.includes(kind)) return false;
54437
54459
  const canonical = canonicalEnvelope(data, kind);
54438
54460
  if (canonical && !validEnvelope(data, this.now())) return false;
54439
- const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
54440
- if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) || data.readiness.hardwareAvailable !== void 0 && typeof data.readiness.hardwareAvailable !== "boolean"))
54441
- return false;
54442
- if (canonical && kind === "state" && !(data.contentHash === null || typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash)))
54443
- return false;
54444
- if (canonical && kind === "configuration/reported" && (!Number.isSafeInteger(data.revision) || data.revision < 1 || typeof data.contentHash !== "string" || !/^[0-9a-f]{64}$/i.test(data.contentHash) || !Array.isArray(data.errors)))
54445
- return false;
54446
- if (canonical && kind === "state" && (typeof data.connected !== "boolean" || !(data.revision === null || Number.isSafeInteger(data.revision) && data.revision >= 0) || !isObject(data.outputs) || data.inputs !== void 0 && !isObject(data.inputs) || ![...Object.values(data.outputs), ...Object.values(isObject(data.inputs) ? data.inputs : {})].every(
54447
- (value) => typeof value === "boolean"
54448
- )))
54449
- return false;
54450
- if (kind === "measurements" && (!identifier(data.channelId) || typeof data.value !== "number" || !Number.isFinite(data.value) || (canonical ? !Number.isSafeInteger(data.value) || !CANONICAL_UNITS.includes(data.unit) || !["live", "cumulative"].includes(data.kind) : !["ampere", "volt", "watt", "percent"].includes(data.unit))))
54451
- return false;
54452
- if (kind === "faults" && !identifier(data.channelId)) return false;
54453
- if (kind === "acknowledgements" && (!identifier(data.id) || !["accepted", "duplicate", "rejected"].includes(data.status)))
54454
- return false;
54461
+ if (!validStatePayload(data, kind, canonical) || !validEventPayload(data, kind, canonical)) return false;
54455
54462
  const oldest = [...this.controllers].find(([, value]) => !value.activeStream)?.[0];
54456
54463
  if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest === void 0) return false;
54457
54464
  if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest !== void 0)
54458
54465
  this.controllers.delete(oldest);
54459
54466
  const state = this.read(id);
54467
+ if (!this.admitIncoming(id, state, kind, data, canonical)) return false;
54468
+ state.touched = this.now();
54469
+ const receivedAt = new Date(this.now()).toISOString();
54470
+ const metadata2 = {
54471
+ sourceAt: canonical ? data.timestamp : null,
54472
+ receivedAt,
54473
+ streamId: canonical ? state.activeStream : null,
54474
+ sequence: canonical ? data.sequence : null
54475
+ };
54476
+ if (kind === "heartbeat") state.heartbeatAt = canonical ? data.timestamp : receivedAt;
54477
+ if (kind === "state") this.applyState(state, data, canonical, metadata2);
54478
+ this.applyEvents(id, state, kind, data, canonical, metadata2);
54479
+ for (const collection2 of [
54480
+ state.cumulativeMeasurements,
54481
+ state.inputs,
54482
+ state.outputs,
54483
+ state.measurements,
54484
+ state.faults,
54485
+ state.acknowledgements
54486
+ ]) {
54487
+ while (Object.keys(collection2).length > MAX_CHANNELS) delete collection2[Object.keys(collection2)[0]];
54488
+ }
54489
+ state.events.push({ kind, receivedAt });
54490
+ state.events = state.events.slice(-50);
54491
+ this.controllers.set(id, state);
54492
+ return true;
54493
+ }
54494
+ admitIncoming(id, state, kind, data, canonical) {
54460
54495
  if (!canonical && kind === "heartbeat" && typeof data.sequence === "number") {
54461
54496
  if (data.sequence < (state.legacyHeartbeatSequence ?? 0)) return false;
54462
54497
  state.legacyHeartbeatSequence = data.sequence;
@@ -54496,43 +54531,38 @@ var WagoDiagnosticsStore = class {
54496
54531
  }
54497
54532
  return false;
54498
54533
  }
54499
- state.touched = this.now();
54500
- const receivedAt = new Date(this.now()).toISOString();
54501
- const metadata2 = {
54502
- sourceAt: canonical ? data.timestamp : null,
54503
- receivedAt,
54504
- streamId: canonical ? state.activeStream : null,
54505
- sequence: canonical ? data.sequence : null
54506
- };
54507
- if (kind === "heartbeat") state.heartbeatAt = canonical ? data.timestamp : receivedAt;
54508
- if (kind === "state") {
54509
- const contentHash = typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash) ? data.contentHash : void 0;
54510
- const hardwareAvailable = isObject(data.readiness) && typeof data.readiness.hardwareAvailable === "boolean" ? data.readiness.hardwareAvailable : void 0;
54511
- if (state.connected !== data.connected || state.revision !== data.revision || state.contentHash !== contentHash || state.hardwareAvailable !== hardwareAvailable) {
54512
- if (state.stateSourceAt || canonical && state.connected === false)
54513
- state.measurementAfter = canonical ? sourceTime(data.timestamp) : this.now();
54514
- state.measurements = /* @__PURE__ */ Object.create(null);
54515
- state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
54516
- }
54517
- state.inputs = /* @__PURE__ */ Object.create(null);
54518
- state.outputs = /* @__PURE__ */ Object.create(null);
54519
- if (typeof data.connected === "boolean") state.connected = data.connected;
54520
- state.revision = Number.isSafeInteger(data.revision) ? data.revision : void 0;
54521
- state.contentHash = contentHash;
54522
- state.hardwareAvailable = hardwareAvailable;
54523
- state.stateSourceAt = canonical ? data.timestamp : void 0;
54524
- if (data.outputs && typeof data.outputs === "object" && !Array.isArray(data.outputs)) {
54525
- for (const [channelId2, value] of Object.entries(data.outputs).slice(0, MAX_CHANNELS)) {
54526
- if (identifier(channelId2) && typeof value === "boolean")
54527
- state.outputs[channelId2] = { kind: "output", value, ...metadata2 };
54528
- }
54529
- }
54530
- if (isObject(data.inputs))
54531
- for (const [channelId2, value] of Object.entries(data.inputs).slice(0, MAX_CHANNELS)) {
54532
- if (identifier(channelId2) && typeof value === "boolean")
54533
- state.inputs[channelId2] = { kind: "input", value, ...metadata2 };
54534
- }
54535
- }
54534
+ return true;
54535
+ }
54536
+ applyState(state, data, canonical, metadata2) {
54537
+ const contentHash = typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash) ? data.contentHash : void 0;
54538
+ const hardwareAvailable = isObject(data.readiness) && typeof data.readiness.hardwareAvailable === "boolean" ? data.readiness.hardwareAvailable : void 0;
54539
+ if (state.connected !== data.connected || state.revision !== data.revision || state.contentHash !== contentHash || state.hardwareAvailable !== hardwareAvailable) {
54540
+ if (state.stateSourceAt || canonical && state.connected === false)
54541
+ state.measurementAfter = canonical ? sourceTime(data.timestamp) : this.now();
54542
+ state.measurements = /* @__PURE__ */ Object.create(null);
54543
+ state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
54544
+ }
54545
+ state.inputs = /* @__PURE__ */ Object.create(null);
54546
+ state.outputs = /* @__PURE__ */ Object.create(null);
54547
+ if (typeof data.connected === "boolean") state.connected = data.connected;
54548
+ state.revision = Number.isSafeInteger(data.revision) ? data.revision : void 0;
54549
+ state.contentHash = contentHash;
54550
+ state.hardwareAvailable = hardwareAvailable;
54551
+ state.stateSourceAt = canonical ? data.timestamp : void 0;
54552
+ if (data.outputs && typeof data.outputs === "object" && !Array.isArray(data.outputs)) {
54553
+ for (const [channelId2, value] of Object.entries(data.outputs).slice(0, MAX_CHANNELS)) {
54554
+ if (identifier(channelId2) && typeof value === "boolean")
54555
+ state.outputs[channelId2] = { kind: "output", value, ...metadata2 };
54556
+ }
54557
+ }
54558
+ if (isObject(data.inputs))
54559
+ for (const [channelId2, value] of Object.entries(data.inputs).slice(0, MAX_CHANNELS)) {
54560
+ if (identifier(channelId2) && typeof value === "boolean")
54561
+ state.inputs[channelId2] = { kind: "input", value, ...metadata2 };
54562
+ }
54563
+ }
54564
+ applyEvents(id, state, kind, data, canonical, metadata2) {
54565
+ const { receivedAt } = metadata2;
54536
54566
  if (kind === "measurements" && identifier(data.channelId) && typeof data.value === "number" && Number.isFinite(data.value) && (canonical ? CANONICAL_UNITS : ["ampere", "volt", "watt", "percent"]).includes(data.unit)) {
54537
54567
  const collection2 = canonical && data.kind === "cumulative" ? state.cumulativeMeasurements : state.measurements;
54538
54568
  collection2[data.channelId] = {
@@ -54566,20 +54596,6 @@ var WagoDiagnosticsStore = class {
54566
54596
  receivedAt,
54567
54597
  errors: safeValidationSummaries(data.errors)
54568
54598
  };
54569
- for (const collection2 of [
54570
- state.cumulativeMeasurements,
54571
- state.inputs,
54572
- state.outputs,
54573
- state.measurements,
54574
- state.faults,
54575
- state.acknowledgements
54576
- ]) {
54577
- while (Object.keys(collection2).length > MAX_CHANNELS) delete collection2[Object.keys(collection2)[0]];
54578
- }
54579
- state.events.push({ kind, receivedAt });
54580
- state.events = state.events.slice(-50);
54581
- this.controllers.set(id, state);
54582
- return true;
54583
54599
  }
54584
54600
  };
54585
54601
 
@@ -56184,14 +56200,14 @@ function wagoFw31IdentityRead(withRoot = false) {
56184
56200
  (path, i) => `printf 'source${i}\\n' && /bin/bash -o pipefail -c ${quotedRead} bash "${withRoot ? "$root" : ""}/${path}"`
56185
56201
  ).join(" && ")} && printf 'complete\\n'; }`;
56186
56202
  }
56187
- function isCc100Fw31Identity(output) {
56188
- if (output.length + (output.endsWith("\n") ? 0 : 1) > 5e4) return false;
56203
+ function parseIdentityFrames(output) {
56204
+ if (output.length + (output.endsWith("\n") ? 0 : 1) > 5e4) return null;
56189
56205
  const chunks = [[], [], []];
56190
56206
  let source = -1;
56191
56207
  let bytes = 0;
56192
56208
  let complete = false;
56193
56209
  for (const line of (output.endsWith("\n") ? output.slice(0, -1) : output).split("\n")) {
56194
- if (complete) return false;
56210
+ if (complete) return null;
56195
56211
  if (line === `source${source + 1}` && source < 2) {
56196
56212
  source++;
56197
56213
  continue;
@@ -56201,25 +56217,35 @@ function isCc100Fw31Identity(output) {
56201
56217
  continue;
56202
56218
  }
56203
56219
  if (source >= 0 && /^ *$/.test(line)) continue;
56204
- if (source < 0 || !/^ *[0-9]+(?: +[0-9]+)* *$/.test(line)) return false;
56220
+ if (source < 0 || !/^ *[0-9]+(?: +[0-9]+)* *$/.test(line)) return null;
56205
56221
  for (const token of line.trim().split(/ +/)) {
56206
56222
  const byte = Number(token);
56207
- if (++bytes > 8192 || byte > 255) return false;
56223
+ if (++bytes > 8192 || byte > 255) return null;
56208
56224
  chunks[source].push(byte);
56209
56225
  }
56210
56226
  }
56211
- if (!complete) return false;
56227
+ if (!complete) return null;
56228
+ return chunks;
56229
+ }
56230
+ function parseIdentityFields(text) {
56231
+ if (/[^\n\x20-\x7e]/.test(text)) return null;
56232
+ const fields2 = /* @__PURE__ */ new Map();
56233
+ for (const line of text.split("\n")) {
56234
+ if (line === "" || line.startsWith("#")) continue;
56235
+ const match = /^([A-Z][A-Z0-9_]*)=(?:"([^"\\]*)"|([^"\\\s]+))$/.exec(line);
56236
+ if (!match || fields2.has(match[1])) return null;
56237
+ fields2.set(match[1], match[2] ?? match[3]);
56238
+ }
56239
+ return fields2;
56240
+ }
56241
+ function isCc100Fw31Identity(output) {
56242
+ const chunks = parseIdentityFrames(output);
56243
+ if (!chunks) return false;
56212
56244
  const texts = chunks.map((chunk) => Buffer.from(chunk).toString("latin1"));
56213
56245
  if (texts[2] !== "CC100-751-9301\0") return false;
56214
56246
  for (let i = 0; i < 2; i++) {
56215
- if (/[^\n\x20-\x7e]/.test(texts[i])) return false;
56216
- const fields2 = /* @__PURE__ */ new Map();
56217
- for (const line of texts[i].split("\n")) {
56218
- if (line === "" || line.startsWith("#")) continue;
56219
- const match = /^([A-Z][A-Z0-9_]*)=(?:"([^"\\]*)"|([^"\\\s]+))$/.exec(line);
56220
- if (!match || fields2.has(match[1])) return false;
56221
- fields2.set(match[1], match[2] ?? match[3]);
56222
- }
56247
+ const fields2 = parseIdentityFields(texts[i]);
56248
+ if (!fields2) return false;
56223
56249
  if (i === 0) {
56224
56250
  if (!["PTXDIST_PLATFORM_NAME", "VERSION", "VERSION_ID"].every((key) => fields2.has(key))) return false;
56225
56251
  for (const [key, value] of fields2) {
@@ -56939,6 +56965,36 @@ WagoCredentialRotationEntity = __decorateClass([
56939
56965
  // backend/wago-credential-rotation.ts
56940
56966
  var CAPABILITY = "credential-rotation-v1";
56941
56967
  var WAIT_MS = 3e4;
56968
+ function assertRotationController(controller, retry) {
56969
+ if (controller.trustState !== "claimed" || !controller.mqttServerId || controller.compatibilityError)
56970
+ throw new import_common4.ConflictException("A compatible claimed controller is required.");
56971
+ if ("credentialMqttServerId" in controller && controller.credentialMqttServerId != null && controller.credentialMqttServerId !== controller.mqttServerId)
56972
+ throw new import_common4.ConflictException("Use the original credential broker before rotating credentials.");
56973
+ let capabilities;
56974
+ try {
56975
+ capabilities = JSON.parse(controller.capabilities);
56976
+ } catch {
56977
+ capabilities = [];
56978
+ }
56979
+ if (!Array.isArray(capabilities) || !capabilities.includes(CAPABILITY))
56980
+ throw new import_common4.ConflictException("Install a runtime supporting credential-rotation-v1 before rotating credentials.");
56981
+ const heartbeatAt = sourceTime(controller.lastHeartbeatAt);
56982
+ const now = Date.now();
56983
+ if (!retry && (heartbeatAt === null || heartbeatAt > now || now - heartbeatAt >= 9e4))
56984
+ throw new import_common4.ConflictException("Wait for a fresh permanent controller heartbeat before rotating credentials.");
56985
+ }
56986
+ function checkPendingRotation(previous, credentialEpoch, retry, mqttServerId, prefix) {
56987
+ if (previous && previous.credentialEpoch !== credentialEpoch)
56988
+ throw new import_common4.ConflictException("Recover the previous credential registration before rotating again.");
56989
+ if (previous && previous.phase !== "completed" && !retry)
56990
+ throw new import_common4.ConflictException("Retry the pending credential handoff instead of rotating again.");
56991
+ if (retry && (!previous || previous.phase === "provisioning"))
56992
+ throw new import_common4.ConflictException("Broker rotation completion is uncertain. Recover broker credentials before retrying.");
56993
+ if (retry && previous?.phase === "completed") return { state: "completed", revision: previous.revision };
56994
+ if (previous && (previous.mqttServerId !== mqttServerId || previous.prefix !== prefix))
56995
+ throw new import_common4.ConflictException("Credential rotation must use its original broker and namespace.");
56996
+ return void 0;
56997
+ }
56942
56998
  var WagoCredentialRotationUncertainError = class extends import_common4.ConflictException {
56943
56999
  constructor() {
56944
57000
  super("Credential rotation is incomplete. Inspect its recovery state and retry the pending handoff.");
@@ -56965,22 +57021,7 @@ var WagoCredentialRotationService = class {
56965
57021
  await guard.assertOwned();
56966
57022
  const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
56967
57023
  if (!controller) throw new import_common4.NotFoundException("WAGO controller not found");
56968
- if (controller.trustState !== "claimed" || !controller.mqttServerId || controller.compatibilityError)
56969
- throw new import_common4.ConflictException("A compatible claimed controller is required.");
56970
- if ("credentialMqttServerId" in controller && controller.credentialMqttServerId != null && controller.credentialMqttServerId !== controller.mqttServerId)
56971
- throw new import_common4.ConflictException("Use the original credential broker before rotating credentials.");
56972
- let capabilities;
56973
- try {
56974
- capabilities = JSON.parse(controller.capabilities);
56975
- } catch {
56976
- capabilities = [];
56977
- }
56978
- if (!Array.isArray(capabilities) || !capabilities.includes(CAPABILITY))
56979
- throw new import_common4.ConflictException("Install a runtime supporting credential-rotation-v1 before rotating credentials.");
56980
- const heartbeatAt = sourceTime(controller.lastHeartbeatAt);
56981
- const now = Date.now();
56982
- if (!retry && (heartbeatAt === null || heartbeatAt > now || now - heartbeatAt >= 9e4))
56983
- throw new import_common4.ConflictException("Wait for a fresh permanent controller heartbeat before rotating credentials.");
57024
+ assertRotationController(controller, retry);
56984
57025
  const [epochRow] = await this.repository.query(
56985
57026
  "SELECT credential_epoch AS epoch FROM plugin_wago_controllers WHERE id = ?",
56986
57027
  [controllerId]
@@ -56991,17 +57032,8 @@ var WagoCredentialRotationService = class {
56991
57032
  prefix = normalizeOperationalPrefix(prefix);
56992
57033
  const identity = `wago-controller-${controller.hardwareId}`;
56993
57034
  const previous = await this.repository.createQueryBuilder("rotation").addSelect("rotation.encryptedCredentials").where("rotation.controllerId = :controllerId", { controllerId }).getOne();
56994
- if (previous && previous.credentialEpoch !== credentialEpoch)
56995
- throw new import_common4.ConflictException("Recover the previous credential registration before rotating again.");
56996
- if (previous && previous.phase !== "completed" && !retry)
56997
- throw new import_common4.ConflictException("Retry the pending credential handoff instead of rotating again.");
56998
- if (retry && (!previous || previous.phase === "provisioning"))
56999
- throw new import_common4.ConflictException(
57000
- "Broker rotation completion is uncertain. Recover broker credentials before retrying."
57001
- );
57002
- if (retry && previous?.phase === "completed") return { state: "completed", revision: previous.revision };
57003
- if (previous && (previous.mqttServerId !== controller.mqttServerId || previous.prefix !== prefix))
57004
- throw new import_common4.ConflictException("Credential rotation must use its original broker and namespace.");
57035
+ const completed = checkPendingRotation(previous, credentialEpoch, retry, controller.mqttServerId, prefix);
57036
+ if (completed) return completed;
57005
57037
  const lifecycle = new WagoAudit(this.context).begin(principal, controllerId, "credential_rotation");
57006
57038
  await lifecycle.attempt();
57007
57039
  let uncertain = retry;
@@ -58717,6 +58749,9 @@ WagoRuntimeArtifactsService = __decorateClass([
58717
58749
  // backend/wago-commissioning-readiness.ts
58718
58750
  var import_common7 = require("@nestjs/common");
58719
58751
  var import_plugins_backend_sdk11 = require("@attraccess/plugins-backend-sdk");
58752
+ function isReadiness(value) {
58753
+ return !!value && typeof value === "object" && "configurationAccepted" in value && typeof value.configurationAccepted === "boolean" && "hardwareAvailable" in value && typeof value.hardwareAvailable === "boolean" && "ready" in value && typeof value.ready === "boolean";
58754
+ }
58720
58755
  var WagoCommissioningReadiness = class {
58721
58756
  constructor(context) {
58722
58757
  __publicField(this, "context", context);
@@ -58755,7 +58790,7 @@ var WagoCommissioningReadiness = class {
58755
58790
  const now = Date.now();
58756
58791
  const value = JSON.parse(message.payload.toString("utf8"));
58757
58792
  const readiness = value.readiness;
58758
- if (timestamp > now || event.contentHash !== null && !/^[a-f0-9]{64}$/i.test(event.contentHash) || !readiness || typeof readiness !== "object" || !("configurationAccepted" in readiness) || typeof readiness.configurationAccepted !== "boolean" || !("hardwareAvailable" in readiness) || typeof readiness.hardwareAvailable !== "boolean" || !("ready" in readiness) || typeof readiness.ready !== "boolean") {
58793
+ if (timestamp > now || event.contentHash !== null && !/^[a-f0-9]{64}$/i.test(event.contentHash) || !isReadiness(readiness)) {
58759
58794
  current.state = void 0;
58760
58795
  return;
58761
58796
  }
@@ -59114,34 +59149,36 @@ function assertManagementKey(key) {
59114
59149
  const inner = read();
59115
59150
  if (offset !== bytes.length || inner.length % 8 || !inner.subarray(0, 4).equals(inner.subarray(4, 8)))
59116
59151
  throw new Error();
59117
- let cursor = 8;
59118
- const part = () => {
59119
- const length = inner.readUInt32BE(cursor);
59120
- cursor += 4;
59121
- if (length > inner.length - cursor) throw new Error();
59122
- const value = inner.subarray(cursor, cursor + length);
59123
- cursor += length;
59124
- return value;
59125
- };
59126
- if (part().toString() !== algorithm) throw new Error();
59127
- const pub = part(), priv = part();
59128
- if (pub.length !== 32 || priv.length !== 64 || !priv.subarray(32).equals(pub) || !publicBlob(pub).equals(blob) || part().length)
59129
- throw new Error();
59130
- const derived = (0, import_node_crypto12.createPublicKey)(
59131
- (0, import_node_crypto12.createPrivateKey)({
59132
- key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), priv.subarray(0, 32)]),
59133
- format: "der",
59134
- type: "pkcs8"
59135
- })
59136
- ).export({ format: "jwk" });
59137
- if (typeof derived.x !== "string" || !Buffer.from(derived.x, "base64url").equals(pub)) throw new Error();
59138
- const padding = inner.subarray(cursor);
59139
- if (!padding.length || padding.length > 8 || !padding.every((value, index) => value === index + 1))
59140
- throw new Error();
59152
+ assertPrivateKeyPayload(inner, blob);
59141
59153
  } catch {
59142
59154
  throw new Error("invalid_key");
59143
59155
  }
59144
59156
  }
59157
+ function assertPrivateKeyPayload(inner, blob) {
59158
+ let cursor = 8;
59159
+ const part = () => {
59160
+ const length = inner.readUInt32BE(cursor);
59161
+ cursor += 4;
59162
+ if (length > inner.length - cursor) throw new Error();
59163
+ const value = inner.subarray(cursor, cursor + length);
59164
+ cursor += length;
59165
+ return value;
59166
+ };
59167
+ if (part().toString() !== algorithm) throw new Error();
59168
+ const pub = part(), priv = part();
59169
+ if (pub.length !== 32 || priv.length !== 64 || !priv.subarray(32).equals(pub) || !publicBlob(pub).equals(blob) || part().length)
59170
+ throw new Error();
59171
+ const derived = (0, import_node_crypto12.createPublicKey)(
59172
+ (0, import_node_crypto12.createPrivateKey)({
59173
+ key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), priv.subarray(0, 32)]),
59174
+ format: "der",
59175
+ type: "pkcs8"
59176
+ })
59177
+ ).export({ format: "jwk" });
59178
+ if (typeof derived.x !== "string" || !Buffer.from(derived.x, "base64url").equals(pub)) throw new Error();
59179
+ const padding = inner.subarray(cursor);
59180
+ if (!padding.length || padding.length > 8 || !padding.every((value, index) => value === index + 1)) throw new Error();
59181
+ }
59145
59182
  function restoreManagementKey(privateKey, expectedFingerprint) {
59146
59183
  try {
59147
59184
  if (privateKey.length > 4096) throw new Error();
@@ -59571,15 +59608,7 @@ var WagoManagementService = class {
59571
59608
  await this.adapter.installKey(tx, input.temporarySsh, key.publicKey);
59572
59609
  await this.step(record2, owner, "verifying_key");
59573
59610
  await this.verify(record2, key.privateKey);
59574
- if (record2.mode === "baseline") {
59575
- await this.step(record2, owner, "restricting_access");
59576
- await this.adapter.restrictAccess(tx, input.temporarySsh, key.privateKey);
59577
- await this.step(record2, owner, "verifying_baseline");
59578
- await this.verify(record2, key.privateKey);
59579
- const result = await this.adapter.verifyBaseline(tx, key.privateKey);
59580
- if (!result.passwordDisabled || !result.defaultAccessDisabled || !result.minimumPrivileges || !result.wbmSecure && !record2.exceptions.includes("wbm_exposed") || !result.otherManagementSecure && !record2.exceptions.includes("other_services_exposed"))
59581
- throw new Error();
59582
- }
59611
+ if (record2.mode === "baseline") await this.enforceBaseline(record2, owner, input.temporarySsh, key.privateKey);
59583
59612
  await this.step(record2, owner, "committing");
59584
59613
  await this.adapter.commit(tx, input.temporarySsh, key.privateKey);
59585
59614
  record2.state = record2.mode === "baseline" && record2.exceptions.length === 0 ? "hardened" : "key_enrolled";
@@ -59596,6 +59625,16 @@ var WagoManagementService = class {
59596
59625
  assertOwned
59597
59626
  );
59598
59627
  }
59628
+ async enforceBaseline(record2, owner, credential, privateKey) {
59629
+ const tx = record2.transaction;
59630
+ await this.step(record2, owner, "restricting_access");
59631
+ await this.adapter.restrictAccess(tx, credential, privateKey);
59632
+ await this.step(record2, owner, "verifying_baseline");
59633
+ await this.verify(record2, privateKey);
59634
+ const result = await this.adapter.verifyBaseline(tx, privateKey);
59635
+ if (!result.passwordDisabled || !result.defaultAccessDisabled || !result.minimumPrivileges || !result.wbmSecure && !record2.exceptions.includes("wbm_exposed") || !result.otherManagementSecure && !record2.exceptions.includes("other_services_exposed"))
59636
+ throw new Error();
59637
+ }
59599
59638
  async recover(controllerId, input, assertOwned) {
59600
59639
  exactKeys2(input, ["confirm", "temporarySsh"]);
59601
59640
  if (input.confirm !== true) throw new ManagementError("invalid_request");
@@ -60671,15 +60710,22 @@ var WagoCommissioningService = class {
60671
60710
  (result) => result.state === "awaiting_discovery"
60672
60711
  );
60673
60712
  }
60674
- async deliverWhileLocked(id, input, principal = null) {
60713
+ async loadDeliverableSession(id) {
60675
60714
  const session = await this.sessions.findOneBy({ id });
60676
60715
  if (!session) throw new import_common8.NotFoundException("commissioning session not found");
60677
60716
  if (!["awaiting_delivery", "delivering", "awaiting_codesys_confirmation", "delivery_failed"].includes(session.state))
60678
60717
  throw new import_common8.ConflictException("commissioning session cannot be delivered in its current state");
60679
- const credential = requireDeliveryCredentials(input);
60680
- if (principal) session.initiatingPrincipal = JSON.stringify(principal);
60718
+ return session;
60719
+ }
60720
+ async requireRuntimeArtifact(session) {
60681
60721
  if (!isRuntimeArtifactConfigured() && !session.runtimeArtifactDigest && !await this.artifacts?.has())
60682
60722
  throw new import_common8.ConflictException("Import a signed CC100 runtime release before installation.");
60723
+ }
60724
+ async deliverWhileLocked(id, input, principal = null) {
60725
+ const session = await this.loadDeliverableSession(id);
60726
+ const credential = requireDeliveryCredentials(input);
60727
+ if (principal) session.initiatingPrincipal = JSON.stringify(principal);
60728
+ await this.requireRuntimeArtifact(session);
60683
60729
  let pairingCode;
60684
60730
  try {
60685
60731
  pairingCode = this.decryptVerifier(session);
@@ -62427,6 +62473,41 @@ var import_plugins_backend_sdk18 = require("@attraccess/plugins-backend-sdk");
62427
62473
  // backend/diagnostics.service.ts
62428
62474
  var import_common11 = require("@nestjs/common");
62429
62475
  var import_plugins_backend_sdk17 = require("@attraccess/plugins-backend-sdk");
62476
+ function hasDraftChanges(draft, latest) {
62477
+ return !!draft && (!latest || configurationHash(JSON.parse(draft.snapshot)) !== latest.contentHash || configurationHash({ metadata: draft.presetProvenance ? JSON.parse(draft.presetProvenance) : null }) !== configurationHash({ metadata: latest.presetProvenance ? JSON.parse(latest.presetProvenance) : null }));
62478
+ }
62479
+ function configurationRejections(latest, runtime) {
62480
+ return latest?.rejectionErrors ? safeValidationSummaries(JSON.parse(latest.rejectionErrors)) : latest && runtime.rejection?.revision === latest.revision && runtime.rejection.contentHash === latest.contentHash ? runtime.rejection.errors : [];
62481
+ }
62482
+ function configurationSummary(draft, latest, applied, runtime, revisionMismatch) {
62483
+ const validationErrors = draft ? validateSnapshot(JSON.parse(draft.snapshot)) : [];
62484
+ return {
62485
+ draftUpdatedAt: draft?.updatedAt ?? null,
62486
+ draftChanged: hasDraftChanges(draft, latest),
62487
+ validationErrorCount: validationErrors.length,
62488
+ // Codes originate in our validator. Omit messages and dynamic paths, which can include arbitrary draft values.
62489
+ validationCodes: [...new Set(validationErrors.map((error) => error.code))].slice(0, 50),
62490
+ validationErrors: safeValidationSummaries(validationErrors),
62491
+ rejectionErrors: configurationRejections(latest, runtime),
62492
+ publishedRevision: latest?.revision ?? null,
62493
+ publishedState: latest?.state ?? null,
62494
+ appliedRevision: applied?.revision ?? null,
62495
+ reportedRevision: runtime.revision ?? null,
62496
+ revisionMismatch,
62497
+ rejected: latest?.state === "rejected"
62498
+ };
62499
+ }
62500
+ function runtimeStreamSummary(runtime) {
62501
+ return {
62502
+ sequenceGaps: runtime.activeStream ? runtime.sequenceGaps : null,
62503
+ activeStream: runtime.activeStream ?? null,
62504
+ trackingExhausted: runtime.trackingExhausted,
62505
+ stateConnected: runtime.connected ?? null,
62506
+ stateHardwareAvailable: runtime.hardwareAvailable ?? null,
62507
+ stateSourceAt: runtime.stateSourceAt ?? null,
62508
+ 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."
62509
+ };
62510
+ }
62430
62511
  function own(values, key) {
62431
62512
  return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : void 0;
62432
62513
  }
@@ -62560,7 +62641,6 @@ var WagoDiagnosticsService = class {
62560
62641
  applied?.revision ?? null,
62561
62642
  Object.fromEntries(appliedSnapshot?.logicalChannels.map((channel) => [channel.id, channel.capabilities]) ?? [])
62562
62643
  );
62563
- const validationErrors = draft ? validateSnapshot(JSON.parse(draft.snapshot)) : [];
62564
62644
  return {
62565
62645
  controllerId,
62566
62646
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -62572,28 +62652,8 @@ var WagoDiagnosticsService = class {
62572
62652
  protocolVersion: controller.protocolVersion,
62573
62653
  capabilities: JSON.parse(controller.capabilities).slice(0, 64),
62574
62654
  incompatible: !!controller.compatibilityError,
62575
- sequenceGaps: runtime.activeStream ? runtime.sequenceGaps : null,
62576
- activeStream: runtime.activeStream ?? null,
62577
- trackingExhausted: runtime.trackingExhausted,
62578
- stateConnected: runtime.connected ?? null,
62579
- stateHardwareAvailable: runtime.hardwareAvailable ?? null,
62580
- stateSourceAt: runtime.stateSourceAt ?? null,
62581
- 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.",
62582
- configuration: {
62583
- draftUpdatedAt: draft?.updatedAt ?? null,
62584
- draftChanged: !!draft && (!latest || configurationHash(JSON.parse(draft.snapshot)) !== latest.contentHash || configurationHash({ metadata: draft.presetProvenance ? JSON.parse(draft.presetProvenance) : null }) !== configurationHash({ metadata: latest.presetProvenance ? JSON.parse(latest.presetProvenance) : null })),
62585
- validationErrorCount: validationErrors.length,
62586
- // Codes originate in our validator. Omit messages and dynamic paths, which can include arbitrary draft values.
62587
- validationCodes: [...new Set(validationErrors.map((error) => error.code))].slice(0, 50),
62588
- validationErrors: safeValidationSummaries(validationErrors),
62589
- rejectionErrors: latest?.rejectionErrors ? safeValidationSummaries(JSON.parse(latest.rejectionErrors)) : latest && runtime.rejection?.revision === latest.revision && runtime.rejection.contentHash === latest.contentHash ? runtime.rejection.errors : [],
62590
- publishedRevision: latest?.revision ?? null,
62591
- publishedState: latest?.state ?? null,
62592
- appliedRevision: applied?.revision ?? null,
62593
- reportedRevision: runtime.revision ?? null,
62594
- revisionMismatch,
62595
- rejected: latest?.state === "rejected"
62596
- },
62655
+ ...runtimeStreamSummary(runtime),
62656
+ configuration: configurationSummary(draft, latest, applied, runtime, revisionMismatch),
62597
62657
  hardwareReadiness: "unknown",
62598
62658
  hardwareReadinessReason: "Reported hardware availability is shown when supplied; it does not prove physical I/O readiness. Applied configuration and cached output state are not physical proof.",
62599
62659
  channels: ((appliedSnapshot ?? snapshot)?.logicalChannels ?? []).slice(0, 256).map((channel) => {
@@ -63015,12 +63075,23 @@ var WagoFlowService = class {
63015
63075
  return;
63016
63076
  }
63017
63077
  const channels = await this.channels(controller.id);
63078
+ const stream = this.admitEvent(controller, event, eventTime);
63079
+ if (!stream) return;
63080
+ if (event.category === "state") {
63081
+ this.applyState(controller, event, eventTime, stream, channels);
63082
+ } else if ("channelId" in event) {
63083
+ const channel = channels.find((channel2) => channel2.id === event.channelId);
63084
+ if (!channel || event.category === "measurement" && !channel.capabilities.includes("measurement")) return;
63085
+ this.store(controller, event.channelId, event, event.category === "measurement" ? event.value : event);
63086
+ }
63087
+ }
63088
+ admitEvent(controller, event, eventTime) {
63018
63089
  let stream = this.streams.get(controller.id);
63019
- if (stream?.exhausted) return;
63090
+ if (stream?.exhausted) return void 0;
63020
63091
  if (!stream || stream.active !== event.streamId) {
63021
63092
  if (event.category !== "state" || stream?.retired.has(event.streamId) || stream && (!event.connected || Date.now() - eventTime > STALE_AFTER_MS2 || eventTime <= stream.latestSourceTime)) {
63022
63093
  this.context.logger.warn(`Ignoring unestablished or retired WAGO stream for ${controller.hardwareId}`);
63023
- return;
63094
+ return void 0;
63024
63095
  }
63025
63096
  if (stream && stream.retired.size >= MAX_RETIRED_STREAMS) {
63026
63097
  stream.exhausted = true;
@@ -63028,7 +63099,7 @@ var WagoFlowService = class {
63028
63099
  this.context.logger.warn(
63029
63100
  `WAGO stream history exhausted for ${controller.hardwareId}; refusing further samples`
63030
63101
  );
63031
- return;
63102
+ return void 0;
63032
63103
  }
63033
63104
  const retired = stream?.retired ?? /* @__PURE__ */ new Set();
63034
63105
  if (stream) retired.add(stream.active);
@@ -63045,7 +63116,7 @@ var WagoFlowService = class {
63045
63116
  const previous = stream.sequences.get(event.category);
63046
63117
  if (previous !== void 0 && event.sequence <= previous) {
63047
63118
  this.context.logger.warn(`Ignoring duplicate or out-of-order WAGO event for ${controller.hardwareId}`);
63048
- return;
63119
+ return void 0;
63049
63120
  }
63050
63121
  if (previous !== void 0 && event.sequence > previous + 1)
63051
63122
  this.context.logger.warn(
@@ -63053,32 +63124,28 @@ var WagoFlowService = class {
63053
63124
  );
63054
63125
  stream.sequences.set(event.category, event.sequence);
63055
63126
  stream.latestSourceTime = Math.max(stream.latestSourceTime, eventTime);
63056
- if (event.category === "state") {
63057
- const wasUnavailable = this.offlineControllers.has(controller.id) || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id) || stream.stateTimestamp !== void 0 && Date.now() - stream.stateTimestamp > STALE_AFTER_MS2;
63058
- stream.stateTimestamp = eventTime;
63059
- if (event.connected) this.offlineControllers.delete(controller.id);
63060
- else this.offlineControllers.add(controller.id);
63061
- if (event.readiness?.hardwareAvailable === false) this.unavailableHardware.add(controller.id);
63062
- else if (event.readiness?.hardwareAvailable === true) this.unavailableHardware.delete(controller.id);
63063
- const applied = this.appliedConfigurations.get(controller.id);
63064
- if (applied && event.revision === applied.revision && event.contentHash === applied.contentHash)
63065
- this.unavailableConfiguration.delete(controller.id);
63066
- else this.unavailableConfiguration.add(controller.id);
63067
- if (wasUnavailable || !event.connected || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id))
63068
- stream.sampleNotBefore = Math.max(stream.sampleNotBefore, eventTime);
63069
- for (const state of this.cache.values()) {
63070
- if (state.controllerId !== controller.id) continue;
63071
- if (wasUnavailable || !event.connected || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id) || state.category === "state")
63072
- state.invalidated = true;
63073
- }
63074
- for (const channel of channels) {
63075
- const value = channel.capabilities.includes("input") && Object.hasOwn(event.inputs ?? {}, channel.id) ? event.inputs[channel.id] : channel.capabilities.includes("output") && Object.hasOwn(event.outputs, channel.id) ? event.outputs[channel.id] : void 0;
63076
- if (typeof value === "boolean") this.store(controller, channel.id, event, value);
63077
- }
63078
- } else if ("channelId" in event) {
63079
- const channel = channels.find((channel2) => channel2.id === event.channelId);
63080
- if (!channel || event.category === "measurement" && !channel.capabilities.includes("measurement")) return;
63081
- this.store(controller, event.channelId, event, event.category === "measurement" ? event.value : event);
63127
+ return stream;
63128
+ }
63129
+ applyState(controller, event, eventTime, stream, channels) {
63130
+ const wasUnavailable = this.offlineControllers.has(controller.id) || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id) || stream.stateTimestamp !== void 0 && Date.now() - stream.stateTimestamp > STALE_AFTER_MS2;
63131
+ stream.stateTimestamp = eventTime;
63132
+ if (event.connected) this.offlineControllers.delete(controller.id);
63133
+ else this.offlineControllers.add(controller.id);
63134
+ if (event.readiness?.hardwareAvailable === false) this.unavailableHardware.add(controller.id);
63135
+ else if (event.readiness?.hardwareAvailable === true) this.unavailableHardware.delete(controller.id);
63136
+ const applied = this.appliedConfigurations.get(controller.id);
63137
+ if (applied && event.revision === applied.revision && event.contentHash === applied.contentHash)
63138
+ this.unavailableConfiguration.delete(controller.id);
63139
+ else this.unavailableConfiguration.add(controller.id);
63140
+ const invalidateSamples = wasUnavailable || !event.connected || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id);
63141
+ if (invalidateSamples) stream.sampleNotBefore = Math.max(stream.sampleNotBefore, eventTime);
63142
+ for (const state of this.cache.values()) {
63143
+ if (state.controllerId !== controller.id) continue;
63144
+ if (invalidateSamples || state.category === "state") state.invalidated = true;
63145
+ }
63146
+ for (const channel of channels) {
63147
+ const value = channel.capabilities.includes("input") && Object.hasOwn(event.inputs ?? {}, channel.id) ? event.inputs[channel.id] : channel.capabilities.includes("output") && Object.hasOwn(event.outputs, channel.id) ? event.outputs[channel.id] : void 0;
63148
+ if (typeof value === "boolean") this.store(controller, channel.id, event, value);
63082
63149
  }
63083
63150
  }
63084
63151
  store(controller, channelId2, event, value) {