@attraccess/plugin-wago 0.1.0-nightly.950.1 → 0.1.0-nightly.952.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 +150 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -51671,6 +51671,13 @@ var CONFIGURATION_CAPABILITY = `configuration-v${CONFIGURATION_PROTOCOL_VERSION}
51671
51671
  var REQUIRED_CAPABILITIES = ["claim", "heartbeat", CONFIGURATION_CAPABILITY];
51672
51672
  var SUPPORTED_PROTOCOL_MAJOR = 1;
51673
51673
  function parseAnnouncement(payload) {
51674
+ const heartbeat = parseHeartbeat(payload);
51675
+ const input = JSON.parse(payload.toString("utf8"));
51676
+ if (typeof input.pairingCode !== "string" || !input.pairingCode.trim())
51677
+ throw new Error("announcement pairingCode is required");
51678
+ return { ...heartbeat, pairingCode: input.pairingCode.trim() };
51679
+ }
51680
+ function parseHeartbeat(payload) {
51674
51681
  let value;
51675
51682
  try {
51676
51683
  value = JSON.parse(payload.toString("utf8"));
@@ -51679,7 +51686,7 @@ function parseAnnouncement(payload) {
51679
51686
  }
51680
51687
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("announcement must be an object");
51681
51688
  const input = value;
51682
- const required = ["hardwareId", "pairingCode", "protocolVersion", "runtimeVersion"];
51689
+ const required = ["hardwareId", "protocolVersion", "runtimeVersion"];
51683
51690
  for (const key of required)
51684
51691
  if (typeof input[key] !== "string" || !input[key].trim()) throw new Error(`announcement ${key} is required`);
51685
51692
  if (!Array.isArray(input.capabilities) || input.capabilities.some((item) => typeof item !== "string"))
@@ -51688,7 +51695,6 @@ function parseAnnouncement(payload) {
51688
51695
  throw new Error("announcement sequence must be a non-negative integer");
51689
51696
  return {
51690
51697
  hardwareId: input.hardwareId.trim(),
51691
- pairingCode: input.pairingCode.trim(),
51692
51698
  enrollmentSecret: typeof input.enrollmentSecret === "string" ? input.enrollmentSecret.trim() : void 0,
51693
51699
  fingerprint: typeof input.fingerprint === "string" ? input.fingerprint.trim() : void 0,
51694
51700
  protocolVersion: input.protocolVersion.trim(),
@@ -51749,7 +51755,7 @@ function parseOperationalMessage(prefix, topic, payload) {
51749
51755
  const value = parseObject(payload, "operational message");
51750
51756
  const timestamp = requiredTimestamp(value.timestamp);
51751
51757
  const sequence = requiredSequence(value.sequence);
51752
- if (value.streamId !== void 0 && (typeof value.streamId !== "string" || !value.streamId.trim() || value.streamId.length > 128))
51758
+ if (typeof value.streamId !== "string" || !value.streamId.trim() || value.streamId.length > 128)
51753
51759
  throw new Error("operational streamId is invalid");
51754
51760
  const streamId = value.streamId;
51755
51761
  if (suffix === "state") {
@@ -51760,7 +51766,7 @@ function parseOperationalMessage(prefix, topic, payload) {
51760
51766
  message: {
51761
51767
  category: "state",
51762
51768
  timestamp,
51763
- ...streamId !== void 0 ? { streamId } : {},
51769
+ streamId,
51764
51770
  sequence,
51765
51771
  connected: value.connected,
51766
51772
  revision: value.revision,
@@ -51772,7 +51778,6 @@ function parseOperationalMessage(prefix, topic, payload) {
51772
51778
  };
51773
51779
  }
51774
51780
  if (suffix === "measurements") {
51775
- if (streamId === void 0) throw new Error("operational streamId is required for measurements");
51776
51781
  return {
51777
51782
  hardwareId,
51778
51783
  message: {
@@ -51792,7 +51797,7 @@ function parseOperationalMessage(prefix, topic, payload) {
51792
51797
  message: {
51793
51798
  category: "fault",
51794
51799
  timestamp,
51795
- ...streamId !== void 0 ? { streamId } : {},
51800
+ streamId,
51796
51801
  sequence,
51797
51802
  channelId: value.channelId,
51798
51803
  code: value.code,
@@ -51807,7 +51812,7 @@ function parseOperationalMessage(prefix, topic, payload) {
51807
51812
  message: {
51808
51813
  category: "acknowledgement",
51809
51814
  timestamp,
51810
- ...streamId !== void 0 ? { streamId } : {},
51815
+ streamId,
51811
51816
  sequence,
51812
51817
  id: value.id,
51813
51818
  status: value.status,
@@ -53430,7 +53435,7 @@ var WagoService = class {
53430
53435
  async onHeartbeat(hardwareId, payload) {
53431
53436
  let heartbeat;
53432
53437
  try {
53433
- heartbeat = parseAnnouncement(payload);
53438
+ heartbeat = parseHeartbeat(payload);
53434
53439
  } catch (error) {
53435
53440
  this.context.logger.warn(
53436
53441
  `Ignoring invalid WAGO heartbeat: ${error instanceof Error ? error.message : String(error)}`
@@ -58179,6 +58184,7 @@ var MAX_CACHE_ENTRIES = 2e3;
58179
58184
  var MAX_PENDING_DISPATCHES = 100;
58180
58185
  var MAX_CONCURRENT_DISPATCHES = 10;
58181
58186
  var MAX_TIMEOUT_MS = 2147483647;
58187
+ var MAX_RETIRED_STREAMS = 128;
58182
58188
  var WagoFlowService = class {
58183
58189
  constructor(context) {
58184
58190
  __publicField(this, "context", context);
@@ -58187,8 +58193,12 @@ var WagoFlowService = class {
58187
58193
  __publicField(this, "settings");
58188
58194
  __publicField(this, "cache", /* @__PURE__ */ new Map());
58189
58195
  __publicField(this, "controllerByHardwareId", /* @__PURE__ */ new Map());
58190
- __publicField(this, "sequences", /* @__PURE__ */ new Map());
58191
- __publicField(this, "sequenceTimestamps", /* @__PURE__ */ new Map());
58196
+ __publicField(this, "streams", /* @__PURE__ */ new Map());
58197
+ __publicField(this, "offlineControllers", /* @__PURE__ */ new Set());
58198
+ __publicField(this, "unavailableHardware", /* @__PURE__ */ new Set());
58199
+ __publicField(this, "unavailableConfiguration", /* @__PURE__ */ new Set());
58200
+ __publicField(this, "appliedConfigurations", /* @__PURE__ */ new Map());
58201
+ __publicField(this, "processingByController", /* @__PURE__ */ new Map());
58192
58202
  __publicField(this, "channelCache", /* @__PURE__ */ new Map());
58193
58203
  __publicField(this, "waiters", /* @__PURE__ */ new Set());
58194
58204
  __publicField(this, "waitersByKey", /* @__PURE__ */ new Map());
@@ -58221,6 +58231,14 @@ var WagoFlowService = class {
58221
58231
  const settings = await this.settings.findOneBy({ id: 1 });
58222
58232
  if (!settings) return;
58223
58233
  const controllers = await this.controllers.find({ where: { trustState: "claimed" } });
58234
+ const controllerIds = new Set(controllers.map((controller) => controller.id));
58235
+ for (const id of this.offlineControllers) if (!controllerIds.has(id)) this.offlineControllers.delete(id);
58236
+ for (const id of this.unavailableHardware) if (!controllerIds.has(id)) this.unavailableHardware.delete(id);
58237
+ for (const id of this.unavailableConfiguration)
58238
+ if (!controllerIds.has(id)) this.unavailableConfiguration.delete(id);
58239
+ for (const id of this.appliedConfigurations.keys())
58240
+ if (!controllerIds.has(id)) this.appliedConfigurations.delete(id);
58241
+ for (const id of this.streams.keys()) if (!controllerIds.has(id)) this.streams.delete(id);
58224
58242
  this.channelCache.clear();
58225
58243
  const revisions = await this.loadLatestAppliedRevisions(controllers.map((controller) => controller.id));
58226
58244
  for (const revision of revisions) this.cacheChannels(revision);
@@ -58295,7 +58313,7 @@ var WagoFlowService = class {
58295
58313
  };
58296
58314
  properties.minimumIntervalMs = { type: "number", title: "Minimum interval (ms)", minimum: 0 };
58297
58315
  if (channel?.capabilities.includes("measurement"))
58298
- properties.minimumChange = { type: "number", title: "Minimum change", minimum: 0 };
58316
+ properties.minimumChange = { type: "number", title: "Minimum change (wire units)", minimum: 0 };
58299
58317
  }
58300
58318
  if (kind !== "event")
58301
58319
  properties.category = {
@@ -58305,7 +58323,10 @@ var WagoFlowService = class {
58305
58323
  oneOf: this.readCategories(channel).map((category) => ({ const: category, title: category }))
58306
58324
  };
58307
58325
  if (kind === "wait") {
58308
- properties.equals = { type: config.category === "measurement" ? "number" : "boolean", title: "Equals" };
58326
+ properties.equals = {
58327
+ type: config.category === "measurement" ? "number" : "boolean",
58328
+ title: config.category === "measurement" ? "Equals (wire value)" : "Equals"
58329
+ };
58309
58330
  properties.timeoutMs = { type: "number", title: "Timeout (ms)", minimum: 1, maximum: MAX_TIMEOUT_MS };
58310
58331
  }
58311
58332
  return { dynamic: true, type: "object", properties, required: ["controllerId", "channelId", "category"] };
@@ -58317,7 +58338,7 @@ var WagoFlowService = class {
58317
58338
  const entries = [...this.cache.values()].filter(
58318
58339
  (state) => state.controllerId === config.controllerId && state.channelId === config.channelId && (!config.category || state.category === config.category)
58319
58340
  );
58320
- return entries.sort((left, right) => right.sequence - left.sequence)[0] ?? null;
58341
+ return entries.sort((left, right) => right.receivedAt - left.receivedAt)[0] ?? null;
58321
58342
  }
58322
58343
  async wait(config) {
58323
58344
  if (typeof config.controllerId !== "number" || typeof config.channelId !== "string" || typeof config.category !== "string")
@@ -58325,8 +58346,8 @@ var WagoFlowService = class {
58325
58346
  const current = this.read(config);
58326
58347
  if (current && this.matchesCondition(current, config)) return current;
58327
58348
  const timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? Math.min(config.timeoutMs, MAX_TIMEOUT_MS) : 3e4;
58349
+ const waiterKey = this.cacheKey(config.controllerId, config.channelId, config.category);
58328
58350
  return new Promise((resolve2) => {
58329
- const waiterKey = this.cacheKey(config.controllerId, config.channelId, config.category);
58330
58351
  const cleanup = () => {
58331
58352
  clearTimeout(timer);
58332
58353
  this.waiters.delete(wake);
@@ -58362,29 +58383,90 @@ var WagoFlowService = class {
58362
58383
  const entry = this.controllerByHardwareId.get(hardwareId);
58363
58384
  if (!entry || entry.serverId !== serverId) return;
58364
58385
  const { controller } = entry;
58365
- const previous = this.sequences.get(controller.id);
58366
58386
  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)) {
58387
+ if (eventTime > Date.now()) {
58388
+ this.context.logger.warn(`Ignoring future-dated WAGO event for ${controller.hardwareId}`);
58389
+ return;
58390
+ }
58391
+ const previous = this.processingByController.get(controller.id) ?? Promise.resolve();
58392
+ const processing = previous.catch(() => void 0).then(() => this.processMessage(controller, event, eventTime));
58393
+ this.processingByController.set(controller.id, processing);
58394
+ try {
58395
+ await processing;
58396
+ } finally {
58397
+ if (this.processingByController.get(controller.id) === processing)
58398
+ this.processingByController.delete(controller.id);
58399
+ }
58400
+ }
58401
+ async processMessage(controller, event, eventTime) {
58402
+ const channels = await this.channels(controller.id);
58403
+ let stream = this.streams.get(controller.id);
58404
+ if (stream?.exhausted) return;
58405
+ if (!stream || stream.active !== event.streamId) {
58406
+ if (event.category !== "state" || stream?.retired.has(event.streamId) || stream && (!event.connected || Date.now() - eventTime > STALE_AFTER_MS2 || eventTime <= stream.latestSourceTime)) {
58407
+ this.context.logger.warn(`Ignoring unestablished or retired WAGO stream for ${controller.hardwareId}`);
58408
+ return;
58409
+ }
58410
+ if (stream && stream.retired.size >= MAX_RETIRED_STREAMS) {
58411
+ stream.exhausted = true;
58412
+ for (const state of this.cache.values()) if (state.controllerId === controller.id) state.invalidated = true;
58413
+ this.context.logger.warn(
58414
+ `WAGO stream history exhausted for ${controller.hardwareId}; refusing further samples`
58415
+ );
58416
+ return;
58417
+ }
58418
+ const retired = stream?.retired ?? /* @__PURE__ */ new Set();
58419
+ if (stream) retired.add(stream.active);
58420
+ stream = {
58421
+ active: event.streamId,
58422
+ latestSourceTime: eventTime,
58423
+ sampleNotBefore: eventTime,
58424
+ retired,
58425
+ sequences: /* @__PURE__ */ new Map()
58426
+ };
58427
+ this.streams.set(controller.id, stream);
58428
+ for (const state of this.cache.values()) if (state.controllerId === controller.id) state.invalidated = true;
58429
+ }
58430
+ const previous = stream.sequences.get(event.category);
58431
+ if (previous !== void 0 && event.sequence <= previous) {
58369
58432
  this.context.logger.warn(`Ignoring duplicate or out-of-order WAGO event for ${controller.hardwareId}`);
58370
58433
  return;
58371
58434
  }
58372
- if (previous !== void 0 && event.sequence <= previous)
58373
- this.context.logger.warn(`Resetting WAGO event sequence after controller reconnect for ${controller.hardwareId}`);
58374
58435
  if (previous !== void 0 && event.sequence > previous + 1)
58375
58436
  this.context.logger.warn(
58376
58437
  `WAGO event sequence gap for ${controller.hardwareId}: ${previous} to ${event.sequence}`
58377
58438
  );
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;
58439
+ stream.sequences.set(event.category, event.sequence);
58440
+ stream.latestSourceTime = Math.max(stream.latestSourceTime, eventTime);
58441
+ if (event.category === "state") {
58442
+ 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;
58443
+ stream.stateTimestamp = eventTime;
58444
+ if (event.connected) this.offlineControllers.delete(controller.id);
58445
+ else this.offlineControllers.add(controller.id);
58446
+ if (event.readiness?.hardwareAvailable === false) this.unavailableHardware.add(controller.id);
58447
+ else if (event.readiness?.hardwareAvailable === true) this.unavailableHardware.delete(controller.id);
58448
+ const applied = this.appliedConfigurations.get(controller.id);
58449
+ if (applied && event.revision === applied.revision && event.contentHash === applied.contentHash)
58450
+ this.unavailableConfiguration.delete(controller.id);
58451
+ else this.unavailableConfiguration.add(controller.id);
58452
+ if (wasUnavailable || !event.connected || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id))
58453
+ stream.sampleNotBefore = Math.max(stream.sampleNotBefore, eventTime);
58454
+ for (const state of this.cache.values()) {
58455
+ if (state.controllerId !== controller.id) continue;
58456
+ if (wasUnavailable || !event.connected || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id) || state.category === "state")
58457
+ state.invalidated = true;
58458
+ }
58459
+ for (const channel of channels) {
58460
+ 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;
58461
+ if (typeof value === "boolean") this.store(controller, channel.id, event, value);
58462
+ }
58463
+ } else if ("channelId" in event) {
58464
+ const channel = channels.find((channel2) => channel2.id === event.channelId);
58465
+ if (!channel || event.category === "measurement" && !channel.capabilities.includes("measurement")) return;
58466
+ this.store(controller, event.channelId, event, event.category === "measurement" ? event.value : event);
58467
+ }
58468
+ }
58469
+ store(controller, channelId, event, value) {
58388
58470
  const state = {
58389
58471
  controllerId: controller.id,
58390
58472
  hardwareId: controller.hardwareId,
@@ -58393,7 +58475,12 @@ var WagoFlowService = class {
58393
58475
  value,
58394
58476
  timestamp: event.timestamp,
58395
58477
  sequence: event.sequence,
58396
- receivedAt: Date.now()
58478
+ streamId: event.streamId,
58479
+ ...event.category === "measurement" ? { unit: event.unit, kind: event.kind } : {},
58480
+ ...event.category === "state" ? { revision: event.revision, contentHash: event.contentHash } : {},
58481
+ receivedAt: Date.now(),
58482
+ offline: this.offlineControllers.has(controller.id),
58483
+ invalidated: this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id) || Date.parse(event.timestamp) < (this.streams.get(controller.id)?.sampleNotBefore ?? 0)
58397
58484
  };
58398
58485
  const cacheKey = this.cacheKey(controller.id, channelId, event.category);
58399
58486
  const previous = this.cache.get(cacheKey);
@@ -58435,6 +58522,16 @@ var WagoFlowService = class {
58435
58522
  cacheChannels(revision) {
58436
58523
  try {
58437
58524
  const channels = JSON.parse(revision.snapshot).logicalChannels;
58525
+ const previous = this.appliedConfigurations.get(revision.controllerId);
58526
+ if (previous?.revision !== revision.revision || previous?.contentHash !== revision.contentHash) {
58527
+ this.unavailableConfiguration.add(revision.controllerId);
58528
+ for (const state of this.cache.values())
58529
+ if (state.controllerId === revision.controllerId) state.invalidated = true;
58530
+ }
58531
+ this.appliedConfigurations.set(revision.controllerId, {
58532
+ revision: revision.revision,
58533
+ contentHash: revision.contentHash
58534
+ });
58438
58535
  this.channelCache.set(revision.controllerId, channels);
58439
58536
  return channels;
58440
58537
  } catch {
@@ -58447,21 +58544,21 @@ var WagoFlowService = class {
58447
58544
  }
58448
58545
  categories(channel) {
58449
58546
  return [
58450
- ...channel?.capabilities.includes("output") ? ["state"] : [],
58547
+ ...channel?.capabilities.some((capability) => capability === "input" || capability === "output") ? ["state"] : [],
58451
58548
  ...channel?.capabilities.includes("measurement") ? ["measurement"] : [],
58452
58549
  "fault"
58453
58550
  ];
58454
58551
  }
58455
58552
  readCategories(channel) {
58456
58553
  return [
58457
- ...channel?.capabilities.includes("output") ? ["state"] : [],
58554
+ ...channel?.capabilities.some((capability) => capability === "input" || capability === "output") ? ["state"] : [],
58458
58555
  ...channel?.capabilities.includes("measurement") ? ["measurement"] : []
58459
58556
  ];
58460
58557
  }
58461
58558
  matchesEvent(config, nodeId, state, previous) {
58462
58559
  if (config.controllerId !== state.controllerId || config.channelId !== state.channelId || config.category !== state.category)
58463
58560
  return false;
58464
- if (typeof config.minimumChange === "number" && typeof state.value === "number" && typeof previous?.value === "number" && Math.abs(state.value - previous.value) < config.minimumChange)
58561
+ 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
58562
  return false;
58466
58563
  if (typeof config.minimumIntervalMs === "number") {
58467
58564
  const lastDispatchAt = this.lastDispatchAtByNode.get(nodeId);
@@ -58471,10 +58568,26 @@ var WagoFlowService = class {
58471
58568
  return true;
58472
58569
  }
58473
58570
  matchesCondition(state, config) {
58474
- return config.equals === void 0 || state.value === config.equals;
58571
+ return this.freshness(state).available && (config.equals === void 0 || state.value === config.equals);
58572
+ }
58573
+ freshness(state) {
58574
+ const age = Date.now() - Date.parse(state.timestamp);
58575
+ const stale = !Number.isFinite(age) || age < 0 || age > STALE_AFTER_MS2;
58576
+ const offline = state.offline === true || this.offlineControllers.has(state.controllerId);
58577
+ const stream = this.streams.get(state.controllerId);
58578
+ const stateTimestamp = stream?.stateTimestamp;
58579
+ const connectionStale = stateTimestamp === void 0 || Date.now() - stateTimestamp > STALE_AFTER_MS2;
58580
+ return {
58581
+ stale,
58582
+ offline,
58583
+ connectionStale,
58584
+ 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)
58585
+ };
58475
58586
  }
58476
58587
  payload(state) {
58477
- return { ...state, stale: Date.now() - Date.parse(state.timestamp) > STALE_AFTER_MS2 };
58588
+ const payload = { ...state };
58589
+ delete payload.invalidated;
58590
+ return { ...payload, ...this.freshness(state) };
58478
58591
  }
58479
58592
  dispatch() {
58480
58593
  while (this.activeDispatches < MAX_CONCURRENT_DISPATCHES && this.dispatches.length) {
@@ -58488,6 +58601,8 @@ var WagoFlowService = class {
58488
58601
  }
58489
58602
  }
58490
58603
  async trigger({ state, previous }) {
58604
+ const stream = this.streams.get(state.controllerId);
58605
+ if (stream?.active !== state.streamId || stream.exhausted) return;
58491
58606
  try {
58492
58607
  await this.context.flows.trigger(
58493
58608
  "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.952.1",
4
4
  "description": "Official Attraccess integration for WAGO CC100 controllers.",
5
5
  "keywords": [
6
6
  "attraccess-plugin",