node-opcua-address-space 2.186.9 → 2.186.10

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.
@@ -14,7 +14,7 @@ import type {
14
14
  } from "node-opcua-address-space-base";
15
15
  import { assert } from "node-opcua-assert";
16
16
  import { AccessLevelFlag, NodeClass, type QualifiedNameLike } from "node-opcua-data-model";
17
- import type { DataValue } from "node-opcua-data-value";
17
+ import { DataValue } from "node-opcua-data-value";
18
18
  import { isMinDate, minDate } from "node-opcua-date-time";
19
19
  import { make_warningLog } from "node-opcua-debug";
20
20
  import type { UAHistoricalDataConfiguration } from "node-opcua-nodeset-ua";
@@ -27,7 +27,7 @@ import {
27
27
  ReadProcessedDetails,
28
28
  ReadRawModifiedDetails
29
29
  } from "node-opcua-service-history";
30
- import { type CallbackT, StatusCodes } from "node-opcua-status-code";
30
+ import { type CallbackT, StatusCode, StatusCodes } from "node-opcua-status-code";
31
31
  import { DataType } from "node-opcua-variant";
32
32
  import type { AddressSpacePrivate } from "../address_space_private.js";
33
33
  import { historizerFactoryHolder } from "../historizer_factory.js";
@@ -88,6 +88,32 @@ function inInTimeRange2(historyReadDetails: ReadRawModifiedDetails, dataValue: D
88
88
  );
89
89
  }
90
90
 
91
+ function _findNearestBefore(q: Dequeue<DataValue>, timeMs: number): DataValue | null {
92
+ let result: DataValue | null = null;
93
+ let c = q.head.next;
94
+ while (c.data) {
95
+ const ts = (c.data.sourceTimestamp || minDate).getTime();
96
+ if (ts >= timeMs) {
97
+ break;
98
+ }
99
+ result = c.data;
100
+ c = c.next;
101
+ }
102
+ return result;
103
+ }
104
+
105
+ function _findNearestAfter(q: Dequeue<DataValue>, timeMs: number): DataValue | null {
106
+ let c = q.head.next;
107
+ while (c.data) {
108
+ const ts = (c.data.sourceTimestamp || minDate).getTime();
109
+ if (ts > timeMs) {
110
+ return c.data;
111
+ }
112
+ c = c.next;
113
+ }
114
+ return null;
115
+ }
116
+
91
117
  function filter_dequeue(
92
118
  q: Dequeue<DataValue>,
93
119
  historyReadRawModifiedDetails: ReadRawModifiedDetails,
@@ -201,6 +227,14 @@ export class VariableHistorian implements IVariableHistorian {
201
227
  }
202
228
  callback(null, dataValues);
203
229
  }
230
+
231
+ public findBoundBefore(date: Date): DataValue | null {
232
+ return _findNearestBefore(this._timeline, date.getTime());
233
+ }
234
+
235
+ public findBoundAfter(date: Date): DataValue | null {
236
+ return _findNearestAfter(this._timeline, date.getTime());
237
+ }
204
238
  }
205
239
 
206
240
  function _get_startOfOfflineArchive(node: UAVariableImpl) {
@@ -354,6 +388,139 @@ function _historyReadRawAsync(
354
388
  this.varHistorian.extractDataValues(historyReadRawModifiedDetails, maxNumberToExtract, isReversed, reverseDataValue, callback);
355
389
  }
356
390
 
391
+ /**
392
+ * Compute a single Bounding Value for the raw-read time domain (OPC 10000-11 6.5.3.2).
393
+ *
394
+ * `before` and `after` are the nearest raw values immediately before and after
395
+ * `boundaryTime` (either may come from outside the requested [startTime, endTime] window).
396
+ * Per 3.1.2 BoundingValues, when neither is missing the Server "determines" a value
397
+ * associated with the boundary time itself, built from that neighbouring data; this
398
+ * function follows the same rule the HistoricalDataConfiguration already uses for the
399
+ * Interpolative Aggregate (Part 13): a sloped linear interpolation, or a held value when
400
+ * the node is configured as Stepped or the value is not numeric.
401
+ *
402
+ * When one side is missing there is nothing to determine the value from, so the result
403
+ * is Bad_BoundNotFound with a null value at the boundary time, as required by 6.5.3.2.
404
+ */
405
+ function _computeBoundingValue(
406
+ node: UAVariableImpl,
407
+ before: DataValue | null,
408
+ after: DataValue | null,
409
+ boundaryTime: Date
410
+ ): DataValue {
411
+ if (!before || !after || !before.sourceTimestamp || !after.sourceTimestamp) {
412
+ return new DataValue({
413
+ sourceTimestamp: boundaryTime,
414
+ statusCode: StatusCodes.BadBoundNotFound,
415
+ value: { dataType: DataType.Null }
416
+ });
417
+ }
418
+
419
+ const stepped = node.$historicalDataConfiguration?.stepped?.readValue()?.value?.value === true;
420
+ const beforeValue = before.value?.value;
421
+ const afterValue = after.value?.value;
422
+ if (stepped || typeof beforeValue !== "number" || typeof afterValue !== "number") {
423
+ // stepped display, or a value that cannot be placed on a slope: hold the value that
424
+ // was in effect at the boundary time (the sample immediately preceding it).
425
+ return new DataValue({
426
+ sourceTimestamp: boundaryTime,
427
+ statusCode: StatusCode.makeStatusCode(before.statusCode, "HistorianInterpolated"),
428
+ value: before.value
429
+ });
430
+ }
431
+
432
+ const t0 = before.sourceTimestamp.getTime();
433
+ const t1 = after.sourceTimestamp.getTime();
434
+ const t = boundaryTime.getTime();
435
+ const coef1 = (t - t0) / (t1 - t0);
436
+ const coef2 = (t1 - t) / (t1 - t0);
437
+ const value = before.value.clone();
438
+ value.value = coef2 * beforeValue + coef1 * afterValue;
439
+ return new DataValue({
440
+ sourceTimestamp: boundaryTime,
441
+ statusCode: StatusCode.makeStatusCode(before.statusCode, "HistorianInterpolated"),
442
+ value
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Add the start and end Bounding Values to a raw-read result when returnBounds is set
448
+ * (OPC 10000-11 6.5.3.2 and 4.6). Only called when both startTime and endTime were
449
+ * specified: the single-sided requests (only startTime, or only endTime) are read to the
450
+ * edge of the online archive rather than a bounded window, so there is no second boundary
451
+ * to compute a bound against, and are left unbounded here.
452
+ *
453
+ * `dataValues` is in the order the client will receive it (already reversed when
454
+ * `reverseDataValue` is set); this function restores chronological order to reason about
455
+ * "first" and "last", then re-applies the same order on the way out.
456
+ */
457
+ function _addBoundingValues(
458
+ node: UAVariableImpl,
459
+ details: ReadRawModifiedDetails,
460
+ dataValues: DataValue[],
461
+ reverseDataValue: boolean
462
+ ): DataValue[] {
463
+ const historian = node.varHistorian;
464
+ const startTime = details.startTime as Date;
465
+ const endTime = details.endTime as Date;
466
+
467
+ const findBefore = (date: Date): DataValue | null => (historian?.findBoundBefore ? historian.findBoundBefore(date) : null);
468
+ const findAfter = (date: Date): DataValue | null => (historian?.findBoundAfter ? historian.findBoundAfter(date) : null);
469
+ const boundNotSupported = (boundaryTime: Date): DataValue =>
470
+ new DataValue({
471
+ sourceTimestamp: boundaryTime,
472
+ statusCode: StatusCodes.BadBoundNotSupported,
473
+ value: { dataType: DataType.Null }
474
+ });
475
+ const supportsBounds = !!(historian?.findBoundBefore && historian.findBoundAfter);
476
+
477
+ const ascending = reverseDataValue ? dataValues.slice().reverse() : dataValues.slice();
478
+
479
+ if (startTime.getTime() === endTime.getTime()) {
480
+ // a request for a single instant: the value at that instant, if any, is already
481
+ // part of dataValues, so there is at most one bound to add.
482
+ if (ascending.length === 0) {
483
+ const bound = supportsBounds
484
+ ? _computeBoundingValue(node, findBefore(startTime), findAfter(startTime), startTime)
485
+ : boundNotSupported(startTime);
486
+ ascending.push(bound);
487
+ }
488
+ return reverseDataValue ? ascending.reverse() : ascending;
489
+ }
490
+
491
+ const result: DataValue[] = [];
492
+
493
+ const hasExactStart = ascending.length > 0 && ascending[0].sourceTimestamp?.getTime() === startTime.getTime();
494
+ if (!hasExactStart) {
495
+ const bound = supportsBounds
496
+ ? _computeBoundingValue(
497
+ node,
498
+ findBefore(startTime),
499
+ ascending.length > 0 ? ascending[0] : findAfter(startTime),
500
+ startTime
501
+ )
502
+ : boundNotSupported(startTime);
503
+ result.push(bound);
504
+ }
505
+
506
+ result.push(...ascending);
507
+
508
+ const hasExactEnd = ascending.length > 0 && ascending[ascending.length - 1].sourceTimestamp?.getTime() === endTime.getTime();
509
+ if (!hasExactEnd) {
510
+ const bound = supportsBounds
511
+ ? _computeBoundingValue(
512
+ node,
513
+ ascending.length > 0 ? ascending[ascending.length - 1] : findBefore(endTime),
514
+ findAfter(endTime),
515
+ endTime
516
+ )
517
+ : boundNotSupported(endTime);
518
+ result.push(bound);
519
+ }
520
+
521
+ return reverseDataValue ? result.reverse() : result;
522
+ }
523
+
357
524
  function _historyReadRaw(
358
525
  this: UAVariable,
359
526
  context: ISessionContext,
@@ -534,6 +701,22 @@ function _historyReadRaw(
534
701
  return callback(err);
535
702
  }
536
703
 
704
+ // Bounding Values (6.4.3.2 / 6.5.3.2): only meaningful when the request has both
705
+ // ends of a time domain to bound. See _addBoundingValues for why the single-sided
706
+ // requests (only startTime, or only endTime) are left unbounded.
707
+ if (
708
+ historyReadRawModifiedDetails.returnBounds &&
709
+ !isMinDate(historyReadRawModifiedDetails.startTime) &&
710
+ !isMinDate(historyReadRawModifiedDetails.endTime)
711
+ ) {
712
+ dataValues = _addBoundingValues(
713
+ this as UAVariableImpl,
714
+ historyReadRawModifiedDetails,
715
+ dataValues,
716
+ reverseDataValue
717
+ );
718
+ }
719
+
537
720
  const cnt = session.continuationPointManager.registerHistoryReadRaw(
538
721
  historyReadRawModifiedDetails.numValuesPerNode,
539
722
  dataValues,
@@ -590,6 +773,7 @@ function _historyRead(
590
773
  statusCode: StatusCodes.BadUserAccessDenied
591
774
  });
592
775
  callback(null, result);
776
+ return;
593
777
  }
594
778
 
595
779
  assert(typeof callback === "function");
@@ -846,4 +1030,6 @@ export function AddressSpace_installHistoricalDataNode(
846
1030
  // update the index of historizing nodes in the addressSpace
847
1031
  node.addressSpace.historizingNodes = node.addressSpace.historizingNodes || new Set();
848
1032
  node.addressSpace.historizingNodes.add(node);
1033
+
1034
+ node.addressSpace.onHistorizingNodeAdded?.(node);
849
1035
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-opcua-address-space",
3
3
  "type": "module",
4
- "version": "2.186.9",
4
+ "version": "2.186.10",
5
5
  "description": "pure nodejs OPCUA SDK - module address-space",
6
6
  "main": "./dist/api/index.js",
7
7
  "types": "./dist/api/index.d.ts",
@@ -23,35 +23,35 @@
23
23
  "@types/semver": "7.8.0",
24
24
  "chalk": "6.0.1",
25
25
  "dequeue": "^1.0.5",
26
- "node-opcua-address-space-base": "2.186.7",
26
+ "node-opcua-address-space-base": "2.186.10",
27
27
  "node-opcua-assert": "2.186.7",
28
28
  "node-opcua-basic-types": "2.186.7",
29
29
  "node-opcua-binary-stream": "2.186.7",
30
- "node-opcua-client-dynamic-extension-object": "2.186.8",
30
+ "node-opcua-client-dynamic-extension-object": "2.186.10",
31
31
  "node-opcua-constants": "2.185.0",
32
32
  "node-opcua-crypto": "6.1.2",
33
- "node-opcua-data-access": "2.186.7",
34
- "node-opcua-data-model": "2.186.7",
35
- "node-opcua-data-value": "2.186.7",
33
+ "node-opcua-data-access": "2.186.10",
34
+ "node-opcua-data-model": "2.186.10",
35
+ "node-opcua-data-value": "2.186.10",
36
36
  "node-opcua-date-time": "2.186.7",
37
37
  "node-opcua-debug": "2.186.7",
38
38
  "node-opcua-enum": "2.186.7",
39
39
  "node-opcua-extension-object": "2.186.7",
40
40
  "node-opcua-factory": "2.186.7",
41
41
  "node-opcua-nodeid": "2.186.7",
42
- "node-opcua-nodeset-ua": "2.186.7",
42
+ "node-opcua-nodeset-ua": "2.186.10",
43
43
  "node-opcua-numeric-range": "2.186.7",
44
44
  "node-opcua-object-registry": "2.186.7",
45
- "node-opcua-pseudo-session": "2.186.8",
46
- "node-opcua-service-browse": "2.186.7",
47
- "node-opcua-service-call": "2.186.7",
48
- "node-opcua-service-history": "2.186.7",
49
- "node-opcua-service-translate-browse-path": "2.186.7",
50
- "node-opcua-service-write": "2.186.7",
45
+ "node-opcua-pseudo-session": "2.186.10",
46
+ "node-opcua-service-browse": "2.186.10",
47
+ "node-opcua-service-call": "2.186.10",
48
+ "node-opcua-service-history": "2.186.10",
49
+ "node-opcua-service-translate-browse-path": "2.186.10",
50
+ "node-opcua-service-write": "2.186.10",
51
51
  "node-opcua-status-code": "2.186.7",
52
- "node-opcua-types": "2.186.7",
52
+ "node-opcua-types": "2.186.10",
53
53
  "node-opcua-utils": "2.186.7",
54
- "node-opcua-variant": "2.186.7",
54
+ "node-opcua-variant": "2.186.10",
55
55
  "node-opcua-xml2json": "2.186.7",
56
56
  "semver": "^7.8.5",
57
57
  "thenify-ex": "4.4.0",
@@ -63,8 +63,8 @@
63
63
  "node-opcua-leak-detector": "2.186.7",
64
64
  "node-opcua-nodesets": "2.185.0",
65
65
  "node-opcua-packet-analyzer": "2.186.7",
66
- "node-opcua-schemas": "2.186.7",
67
- "node-opcua-service-filter": "2.186.7",
66
+ "node-opcua-schemas": "2.186.10",
67
+ "node-opcua-service-filter": "2.186.10",
68
68
  "node-opcua-test-fixtures": "2.184.0",
69
69
  "node-opcua-test-helpers": "2.184.0"
70
70
  },
@@ -83,7 +83,7 @@
83
83
  "internet of things"
84
84
  ],
85
85
  "homepage": "http://node-opcua.github.io/",
86
- "gitHead": "b63bbd461cbfc04d32fb39da3b15c88448867e7d",
86
+ "gitHead": "f6e25f12e17feabaf6ac2b07621b8bfbf92375c9",
87
87
  "files": [
88
88
  "dist",
89
89
  "distHelpers",