@attraccess/plugin-wago 0.1.0-nightly.950.1 → 0.1.0-nightly.951.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.
Files changed (2) hide show
  1. package/dist/index.js +141 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -51749,7 +51749,7 @@ function parseOperationalMessage(prefix, topic, payload) {
51749
51749
  const value = parseObject(payload, "operational message");
51750
51750
  const timestamp = requiredTimestamp(value.timestamp);
51751
51751
  const sequence = requiredSequence(value.sequence);
51752
- if (value.streamId !== void 0 && (typeof value.streamId !== "string" || !value.streamId.trim() || value.streamId.length > 128))
51752
+ if (typeof value.streamId !== "string" || !value.streamId.trim() || value.streamId.length > 128)
51753
51753
  throw new Error("operational streamId is invalid");
51754
51754
  const streamId = value.streamId;
51755
51755
  if (suffix === "state") {
@@ -51760,7 +51760,7 @@ function parseOperationalMessage(prefix, topic, payload) {
51760
51760
  message: {
51761
51761
  category: "state",
51762
51762
  timestamp,
51763
- ...streamId !== void 0 ? { streamId } : {},
51763
+ streamId,
51764
51764
  sequence,
51765
51765
  connected: value.connected,
51766
51766
  revision: value.revision,
@@ -51772,7 +51772,6 @@ function parseOperationalMessage(prefix, topic, payload) {
51772
51772
  };
51773
51773
  }
51774
51774
  if (suffix === "measurements") {
51775
- if (streamId === void 0) throw new Error("operational streamId is required for measurements");
51776
51775
  return {
51777
51776
  hardwareId,
51778
51777
  message: {
@@ -51792,7 +51791,7 @@ function parseOperationalMessage(prefix, topic, payload) {
51792
51791
  message: {
51793
51792
  category: "fault",
51794
51793
  timestamp,
51795
- ...streamId !== void 0 ? { streamId } : {},
51794
+ streamId,
51796
51795
  sequence,
51797
51796
  channelId: value.channelId,
51798
51797
  code: value.code,
@@ -51807,7 +51806,7 @@ function parseOperationalMessage(prefix, topic, payload) {
51807
51806
  message: {
51808
51807
  category: "acknowledgement",
51809
51808
  timestamp,
51810
- ...streamId !== void 0 ? { streamId } : {},
51809
+ streamId,
51811
51810
  sequence,
51812
51811
  id: value.id,
51813
51812
  status: value.status,
@@ -58179,6 +58178,7 @@ var MAX_CACHE_ENTRIES = 2e3;
58179
58178
  var MAX_PENDING_DISPATCHES = 100;
58180
58179
  var MAX_CONCURRENT_DISPATCHES = 10;
58181
58180
  var MAX_TIMEOUT_MS = 2147483647;
58181
+ var MAX_RETIRED_STREAMS = 128;
58182
58182
  var WagoFlowService = class {
58183
58183
  constructor(context) {
58184
58184
  __publicField(this, "context", context);
@@ -58187,8 +58187,12 @@ var WagoFlowService = class {
58187
58187
  __publicField(this, "settings");
58188
58188
  __publicField(this, "cache", /* @__PURE__ */ new Map());
58189
58189
  __publicField(this, "controllerByHardwareId", /* @__PURE__ */ new Map());
58190
- __publicField(this, "sequences", /* @__PURE__ */ new Map());
58191
- __publicField(this, "sequenceTimestamps", /* @__PURE__ */ new Map());
58190
+ __publicField(this, "streams", /* @__PURE__ */ new Map());
58191
+ __publicField(this, "offlineControllers", /* @__PURE__ */ new Set());
58192
+ __publicField(this, "unavailableHardware", /* @__PURE__ */ new Set());
58193
+ __publicField(this, "unavailableConfiguration", /* @__PURE__ */ new Set());
58194
+ __publicField(this, "appliedConfigurations", /* @__PURE__ */ new Map());
58195
+ __publicField(this, "processingByController", /* @__PURE__ */ new Map());
58192
58196
  __publicField(this, "channelCache", /* @__PURE__ */ new Map());
58193
58197
  __publicField(this, "waiters", /* @__PURE__ */ new Set());
58194
58198
  __publicField(this, "waitersByKey", /* @__PURE__ */ new Map());
@@ -58221,6 +58225,14 @@ var WagoFlowService = class {
58221
58225
  const settings = await this.settings.findOneBy({ id: 1 });
58222
58226
  if (!settings) return;
58223
58227
  const controllers = await this.controllers.find({ where: { trustState: "claimed" } });
58228
+ const controllerIds = new Set(controllers.map((controller) => controller.id));
58229
+ for (const id of this.offlineControllers) if (!controllerIds.has(id)) this.offlineControllers.delete(id);
58230
+ for (const id of this.unavailableHardware) if (!controllerIds.has(id)) this.unavailableHardware.delete(id);
58231
+ for (const id of this.unavailableConfiguration)
58232
+ if (!controllerIds.has(id)) this.unavailableConfiguration.delete(id);
58233
+ for (const id of this.appliedConfigurations.keys())
58234
+ if (!controllerIds.has(id)) this.appliedConfigurations.delete(id);
58235
+ for (const id of this.streams.keys()) if (!controllerIds.has(id)) this.streams.delete(id);
58224
58236
  this.channelCache.clear();
58225
58237
  const revisions = await this.loadLatestAppliedRevisions(controllers.map((controller) => controller.id));
58226
58238
  for (const revision of revisions) this.cacheChannels(revision);
@@ -58295,7 +58307,7 @@ var WagoFlowService = class {
58295
58307
  };
58296
58308
  properties.minimumIntervalMs = { type: "number", title: "Minimum interval (ms)", minimum: 0 };
58297
58309
  if (channel?.capabilities.includes("measurement"))
58298
- properties.minimumChange = { type: "number", title: "Minimum change", minimum: 0 };
58310
+ properties.minimumChange = { type: "number", title: "Minimum change (wire units)", minimum: 0 };
58299
58311
  }
58300
58312
  if (kind !== "event")
58301
58313
  properties.category = {
@@ -58305,7 +58317,10 @@ var WagoFlowService = class {
58305
58317
  oneOf: this.readCategories(channel).map((category) => ({ const: category, title: category }))
58306
58318
  };
58307
58319
  if (kind === "wait") {
58308
- properties.equals = { type: config.category === "measurement" ? "number" : "boolean", title: "Equals" };
58320
+ properties.equals = {
58321
+ type: config.category === "measurement" ? "number" : "boolean",
58322
+ title: config.category === "measurement" ? "Equals (wire value)" : "Equals"
58323
+ };
58309
58324
  properties.timeoutMs = { type: "number", title: "Timeout (ms)", minimum: 1, maximum: MAX_TIMEOUT_MS };
58310
58325
  }
58311
58326
  return { dynamic: true, type: "object", properties, required: ["controllerId", "channelId", "category"] };
@@ -58317,7 +58332,7 @@ var WagoFlowService = class {
58317
58332
  const entries = [...this.cache.values()].filter(
58318
58333
  (state) => state.controllerId === config.controllerId && state.channelId === config.channelId && (!config.category || state.category === config.category)
58319
58334
  );
58320
- return entries.sort((left, right) => right.sequence - left.sequence)[0] ?? null;
58335
+ return entries.sort((left, right) => right.receivedAt - left.receivedAt)[0] ?? null;
58321
58336
  }
58322
58337
  async wait(config) {
58323
58338
  if (typeof config.controllerId !== "number" || typeof config.channelId !== "string" || typeof config.category !== "string")
@@ -58325,8 +58340,8 @@ var WagoFlowService = class {
58325
58340
  const current = this.read(config);
58326
58341
  if (current && this.matchesCondition(current, config)) return current;
58327
58342
  const timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? Math.min(config.timeoutMs, MAX_TIMEOUT_MS) : 3e4;
58343
+ const waiterKey = this.cacheKey(config.controllerId, config.channelId, config.category);
58328
58344
  return new Promise((resolve2) => {
58329
- const waiterKey = this.cacheKey(config.controllerId, config.channelId, config.category);
58330
58345
  const cleanup = () => {
58331
58346
  clearTimeout(timer);
58332
58347
  this.waiters.delete(wake);
@@ -58362,29 +58377,90 @@ var WagoFlowService = class {
58362
58377
  const entry = this.controllerByHardwareId.get(hardwareId);
58363
58378
  if (!entry || entry.serverId !== serverId) return;
58364
58379
  const { controller } = entry;
58365
- const previous = this.sequences.get(controller.id);
58366
58380
  const eventTime = Date.parse(event.timestamp);
58367
- const previousTime = this.sequenceTimestamps.get(controller.id);
58368
- if (previous !== void 0 && event.sequence <= previous && (!previousTime || eventTime <= previousTime)) {
58381
+ if (eventTime > Date.now()) {
58382
+ this.context.logger.warn(`Ignoring future-dated WAGO event for ${controller.hardwareId}`);
58383
+ return;
58384
+ }
58385
+ const previous = this.processingByController.get(controller.id) ?? Promise.resolve();
58386
+ const processing = previous.catch(() => void 0).then(() => this.processMessage(controller, event, eventTime));
58387
+ this.processingByController.set(controller.id, processing);
58388
+ try {
58389
+ await processing;
58390
+ } finally {
58391
+ if (this.processingByController.get(controller.id) === processing)
58392
+ this.processingByController.delete(controller.id);
58393
+ }
58394
+ }
58395
+ async processMessage(controller, event, eventTime) {
58396
+ const channels = await this.channels(controller.id);
58397
+ let stream = this.streams.get(controller.id);
58398
+ if (stream?.exhausted) return;
58399
+ if (!stream || stream.active !== event.streamId) {
58400
+ if (event.category !== "state" || stream?.retired.has(event.streamId) || stream && (!event.connected || Date.now() - eventTime > STALE_AFTER_MS2 || eventTime <= stream.latestSourceTime)) {
58401
+ this.context.logger.warn(`Ignoring unestablished or retired WAGO stream for ${controller.hardwareId}`);
58402
+ return;
58403
+ }
58404
+ if (stream && stream.retired.size >= MAX_RETIRED_STREAMS) {
58405
+ stream.exhausted = true;
58406
+ for (const state of this.cache.values()) if (state.controllerId === controller.id) state.invalidated = true;
58407
+ this.context.logger.warn(
58408
+ `WAGO stream history exhausted for ${controller.hardwareId}; refusing further samples`
58409
+ );
58410
+ return;
58411
+ }
58412
+ const retired = stream?.retired ?? /* @__PURE__ */ new Set();
58413
+ if (stream) retired.add(stream.active);
58414
+ stream = {
58415
+ active: event.streamId,
58416
+ latestSourceTime: eventTime,
58417
+ sampleNotBefore: eventTime,
58418
+ retired,
58419
+ sequences: /* @__PURE__ */ new Map()
58420
+ };
58421
+ this.streams.set(controller.id, stream);
58422
+ for (const state of this.cache.values()) if (state.controllerId === controller.id) state.invalidated = true;
58423
+ }
58424
+ const previous = stream.sequences.get(event.category);
58425
+ if (previous !== void 0 && event.sequence <= previous) {
58369
58426
  this.context.logger.warn(`Ignoring duplicate or out-of-order WAGO event for ${controller.hardwareId}`);
58370
58427
  return;
58371
58428
  }
58372
- if (previous !== void 0 && event.sequence <= previous)
58373
- this.context.logger.warn(`Resetting WAGO event sequence after controller reconnect for ${controller.hardwareId}`);
58374
58429
  if (previous !== void 0 && event.sequence > previous + 1)
58375
58430
  this.context.logger.warn(
58376
58431
  `WAGO event sequence gap for ${controller.hardwareId}: ${previous} to ${event.sequence}`
58377
58432
  );
58378
- this.sequences.set(controller.id, event.sequence);
58379
- this.sequenceTimestamps.set(controller.id, eventTime);
58380
- const channelId = event.category === "state" ? null : "channelId" in event ? event.channelId : null;
58381
- if (event.category === "state")
58382
- for (const [id, value] of Object.entries(event.outputs)) await this.store(controller, id, event, value);
58383
- else if (channelId)
58384
- await this.store(controller, channelId, event, event.category === "measurement" ? event.value : event);
58385
- }
58386
- async store(controller, channelId, event, value) {
58387
- if (!(await this.channels(controller.id)).some((channel) => channel.id === channelId)) return;
58433
+ stream.sequences.set(event.category, event.sequence);
58434
+ stream.latestSourceTime = Math.max(stream.latestSourceTime, eventTime);
58435
+ if (event.category === "state") {
58436
+ 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;
58437
+ stream.stateTimestamp = eventTime;
58438
+ if (event.connected) this.offlineControllers.delete(controller.id);
58439
+ else this.offlineControllers.add(controller.id);
58440
+ if (event.readiness?.hardwareAvailable === false) this.unavailableHardware.add(controller.id);
58441
+ else if (event.readiness?.hardwareAvailable === true) this.unavailableHardware.delete(controller.id);
58442
+ const applied = this.appliedConfigurations.get(controller.id);
58443
+ if (applied && event.revision === applied.revision && event.contentHash === applied.contentHash)
58444
+ this.unavailableConfiguration.delete(controller.id);
58445
+ else this.unavailableConfiguration.add(controller.id);
58446
+ if (wasUnavailable || !event.connected || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id))
58447
+ stream.sampleNotBefore = Math.max(stream.sampleNotBefore, eventTime);
58448
+ for (const state of this.cache.values()) {
58449
+ if (state.controllerId !== controller.id) continue;
58450
+ if (wasUnavailable || !event.connected || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id) || state.category === "state")
58451
+ state.invalidated = true;
58452
+ }
58453
+ for (const channel of channels) {
58454
+ 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;
58455
+ if (typeof value === "boolean") this.store(controller, channel.id, event, value);
58456
+ }
58457
+ } else if ("channelId" in event) {
58458
+ const channel = channels.find((channel2) => channel2.id === event.channelId);
58459
+ if (!channel || event.category === "measurement" && !channel.capabilities.includes("measurement")) return;
58460
+ this.store(controller, event.channelId, event, event.category === "measurement" ? event.value : event);
58461
+ }
58462
+ }
58463
+ store(controller, channelId, event, value) {
58388
58464
  const state = {
58389
58465
  controllerId: controller.id,
58390
58466
  hardwareId: controller.hardwareId,
@@ -58393,7 +58469,12 @@ var WagoFlowService = class {
58393
58469
  value,
58394
58470
  timestamp: event.timestamp,
58395
58471
  sequence: event.sequence,
58396
- receivedAt: Date.now()
58472
+ streamId: event.streamId,
58473
+ ...event.category === "measurement" ? { unit: event.unit, kind: event.kind } : {},
58474
+ ...event.category === "state" ? { revision: event.revision, contentHash: event.contentHash } : {},
58475
+ receivedAt: Date.now(),
58476
+ offline: this.offlineControllers.has(controller.id),
58477
+ invalidated: this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id) || Date.parse(event.timestamp) < (this.streams.get(controller.id)?.sampleNotBefore ?? 0)
58397
58478
  };
58398
58479
  const cacheKey = this.cacheKey(controller.id, channelId, event.category);
58399
58480
  const previous = this.cache.get(cacheKey);
@@ -58435,6 +58516,16 @@ var WagoFlowService = class {
58435
58516
  cacheChannels(revision) {
58436
58517
  try {
58437
58518
  const channels = JSON.parse(revision.snapshot).logicalChannels;
58519
+ const previous = this.appliedConfigurations.get(revision.controllerId);
58520
+ if (previous?.revision !== revision.revision || previous?.contentHash !== revision.contentHash) {
58521
+ this.unavailableConfiguration.add(revision.controllerId);
58522
+ for (const state of this.cache.values())
58523
+ if (state.controllerId === revision.controllerId) state.invalidated = true;
58524
+ }
58525
+ this.appliedConfigurations.set(revision.controllerId, {
58526
+ revision: revision.revision,
58527
+ contentHash: revision.contentHash
58528
+ });
58438
58529
  this.channelCache.set(revision.controllerId, channels);
58439
58530
  return channels;
58440
58531
  } catch {
@@ -58447,21 +58538,21 @@ var WagoFlowService = class {
58447
58538
  }
58448
58539
  categories(channel) {
58449
58540
  return [
58450
- ...channel?.capabilities.includes("output") ? ["state"] : [],
58541
+ ...channel?.capabilities.some((capability) => capability === "input" || capability === "output") ? ["state"] : [],
58451
58542
  ...channel?.capabilities.includes("measurement") ? ["measurement"] : [],
58452
58543
  "fault"
58453
58544
  ];
58454
58545
  }
58455
58546
  readCategories(channel) {
58456
58547
  return [
58457
- ...channel?.capabilities.includes("output") ? ["state"] : [],
58548
+ ...channel?.capabilities.some((capability) => capability === "input" || capability === "output") ? ["state"] : [],
58458
58549
  ...channel?.capabilities.includes("measurement") ? ["measurement"] : []
58459
58550
  ];
58460
58551
  }
58461
58552
  matchesEvent(config, nodeId, state, previous) {
58462
58553
  if (config.controllerId !== state.controllerId || config.channelId !== state.channelId || config.category !== state.category)
58463
58554
  return false;
58464
- if (typeof config.minimumChange === "number" && typeof state.value === "number" && typeof previous?.value === "number" && Math.abs(state.value - previous.value) < config.minimumChange)
58555
+ if (typeof config.minimumChange === "number" && typeof state.value === "number" && typeof previous?.value === "number" && previous.streamId === state.streamId && previous.unit === state.unit && previous.kind === state.kind && Math.abs(state.value - previous.value) < config.minimumChange)
58465
58556
  return false;
58466
58557
  if (typeof config.minimumIntervalMs === "number") {
58467
58558
  const lastDispatchAt = this.lastDispatchAtByNode.get(nodeId);
@@ -58471,10 +58562,26 @@ var WagoFlowService = class {
58471
58562
  return true;
58472
58563
  }
58473
58564
  matchesCondition(state, config) {
58474
- return config.equals === void 0 || state.value === config.equals;
58565
+ return this.freshness(state).available && (config.equals === void 0 || state.value === config.equals);
58566
+ }
58567
+ freshness(state) {
58568
+ const age = Date.now() - Date.parse(state.timestamp);
58569
+ const stale = !Number.isFinite(age) || age < 0 || age > STALE_AFTER_MS2;
58570
+ const offline = state.offline === true || this.offlineControllers.has(state.controllerId);
58571
+ const stream = this.streams.get(state.controllerId);
58572
+ const stateTimestamp = stream?.stateTimestamp;
58573
+ const connectionStale = stateTimestamp === void 0 || Date.now() - stateTimestamp > STALE_AFTER_MS2;
58574
+ return {
58575
+ stale,
58576
+ offline,
58577
+ connectionStale,
58578
+ available: !stale && !offline && !connectionStale && stream?.active === state.streamId && !stream.exhausted && Date.parse(state.timestamp) >= stream.sampleNotBefore && !state.invalidated && !this.unavailableHardware.has(state.controllerId) && !this.unavailableConfiguration.has(state.controllerId)
58579
+ };
58475
58580
  }
58476
58581
  payload(state) {
58477
- return { ...state, stale: Date.now() - Date.parse(state.timestamp) > STALE_AFTER_MS2 };
58582
+ const payload = { ...state };
58583
+ delete payload.invalidated;
58584
+ return { ...payload, ...this.freshness(state) };
58478
58585
  }
58479
58586
  dispatch() {
58480
58587
  while (this.activeDispatches < MAX_CONCURRENT_DISPATCHES && this.dispatches.length) {
@@ -58488,6 +58595,8 @@ var WagoFlowService = class {
58488
58595
  }
58489
58596
  }
58490
58597
  async trigger({ state, previous }) {
58598
+ const stream = this.streams.get(state.controllerId);
58599
+ if (stream?.active !== state.streamId || stream.exhausted) return;
58491
58600
  try {
58492
58601
  await this.context.flows.trigger(
58493
58602
  "plugin.wago.event-received",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attraccess/plugin-wago",
3
- "version": "0.1.0-nightly.950.1",
3
+ "version": "0.1.0-nightly.951.1",
4
4
  "description": "Official Attraccess integration for WAGO CC100 controllers.",
5
5
  "keywords": [
6
6
  "attraccess-plugin",