@yarkivaev/scada 1.1.6 → 1.2.0

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/index.js CHANGED
@@ -35,6 +35,10 @@ export { default as meltingRuleEngine } from './src/meltingRuleEngine.js';
35
35
  export { alert, acknowledgedAlert } from './src/alert.js';
36
36
  export { default as alerts } from './src/alerts.js';
37
37
 
38
+ // Timeline
39
+ export { default as segments } from './src/segments.js';
40
+ export { default as requests } from './src/requests.js';
41
+
38
42
  // Utilities
39
43
  export { default as interval } from './src/interval.js';
40
44
  export { default as initialized } from './src/initialized.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yarkivaev/scada",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "SCADA domain objects for plant monitoring",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,7 +39,7 @@ export default function clickhouseSensor(connection, topic, displayName, unit) {
39
39
  if (rows.length === 0) {
40
40
  return { timestamp: new Date(), value: 0, unit };
41
41
  }
42
- return { timestamp: new Date(rows[0].ts), value: rows[0].value, unit };
42
+ return { timestamp: new Date(`${rows[0].ts}Z`), value: rows[0].value, unit };
43
43
  },
44
44
  async measurements(range, step) {
45
45
  const seconds = Math.max(1, Math.floor(step / 1000));
@@ -60,7 +60,7 @@ export default function clickhouseSensor(connection, topic, displayName, unit) {
60
60
  }
61
61
  );
62
62
  return rows.map((row) => {
63
- return { timestamp: new Date(row.ts), value: row.value, unit };
63
+ return { timestamp: new Date(`${row.ts}Z`), value: row.value, unit };
64
64
  });
65
65
  },
66
66
  stream(since, step, callback) {
@@ -74,7 +74,7 @@ export default function clickhouseSensor(connection, topic, displayName, unit) {
74
74
  { topic, since: formatDateTime(lastTs) }
75
75
  );
76
76
  rows.forEach((row) => {
77
- const timestamp = new Date(row.ts);
77
+ const timestamp = new Date(`${row.ts}Z`);
78
78
  callback({ timestamp, value: row.value, unit });
79
79
  lastTs = timestamp;
80
80
  });
@@ -0,0 +1,44 @@
1
+ import pubsub from './pubsub.js';
2
+
3
+ /**
4
+ * Per-machine in-memory label request collection.
5
+ * Stores requests for segment labeling with options for the user to choose from.
6
+ * Supports responding to requests and streaming events via pubsub.
7
+ *
8
+ * @returns {object} collection with add, respond, query, stream methods
9
+ *
10
+ * @example
11
+ * const reqs = requests();
12
+ * reqs.add({ id: 'req-0', name: 'unknown', startTime: new Date(), endTime: new Date(), duration: 60, options: ['on', 'off'] });
13
+ * reqs.respond('req-0', { label: 'on' }); // marks resolved
14
+ * reqs.query(); // returns unresolved requests
15
+ * const sub = reqs.stream((e) => console.log(e));
16
+ * sub.cancel();
17
+ */
18
+ export default function requests() {
19
+ const items = [];
20
+ const bus = pubsub();
21
+ return {
22
+ add(request) {
23
+ items.push({ ...request, resolved: false });
24
+ bus.emit({ type: 'created', request });
25
+ },
26
+ respond(id, body) {
27
+ const found = items.find((r) => {
28
+ return r.id === id && !r.resolved;
29
+ });
30
+ if (!found) {
31
+ return undefined;
32
+ }
33
+ found.resolved = true;
34
+ bus.emit({ type: 'resolved', request: found });
35
+ return { id, ...body };
36
+ },
37
+ query() {
38
+ return items.filter((r) => {
39
+ return !r.resolved;
40
+ });
41
+ },
42
+ stream: bus.stream
43
+ };
44
+ }
@@ -0,0 +1,52 @@
1
+ import pubsub from './pubsub.js';
2
+
3
+ /**
4
+ * Per-machine in-memory timeline segment collection.
5
+ * Stores segments with name, startTime, endTime, duration.
6
+ * Supports relabeling by startTime and streaming events via pubsub.
7
+ *
8
+ * @returns {object} collection with add, relabel, query, stream methods
9
+ *
10
+ * @example
11
+ * const segs = segments();
12
+ * segs.add({ name: 'on', startTime: new Date(), endTime: new Date(), duration: 60 });
13
+ * segs.relabel(startTime, 'heating');
14
+ * segs.query(); // all segments
15
+ * segs.query({ from: '2024-01-01', to: '2024-01-02' }); // filtered
16
+ * const sub = segs.stream((e) => console.log(e));
17
+ * sub.cancel();
18
+ */
19
+ export default function segments() {
20
+ const items = [];
21
+ const bus = pubsub();
22
+ return {
23
+ add(segment) {
24
+ items.push(segment);
25
+ bus.emit({ type: 'created', segment });
26
+ },
27
+ relabel(start, name) {
28
+ const found = items.find((item) => {
29
+ return item.startTime.getTime() === start.getTime();
30
+ });
31
+ if (found) {
32
+ found.name = name;
33
+ bus.emit({ type: 'relabeled', segment: found });
34
+ }
35
+ },
36
+ query(options) {
37
+ if (!options) {
38
+ return items.slice();
39
+ }
40
+ return items.filter((item) => {
41
+ if (options.from && item.endTime < new Date(options.from)) {
42
+ return false;
43
+ }
44
+ if (options.to && item.startTime > new Date(options.to)) {
45
+ return false;
46
+ }
47
+ return true;
48
+ });
49
+ },
50
+ stream: bus.stream
51
+ };
52
+ }