@yarkivaev/scada 1.1.6 → 1.3.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.3.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,21 +60,22 @@ 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
- stream(since, step, callback) {
66
+ stream(since, step, callback, clock) {
67
+ const time = clock || (() => { return new Date(); });
67
68
  let lastTs = since;
68
69
  const timer = setInterval(async () => {
69
70
  try {
70
71
  const rows = await connection.query(
71
72
  `SELECT ts, value FROM scada.metrics
72
- WHERE topic = {topic:String} AND ts > {since:DateTime64(3)}
73
+ WHERE topic = {topic:String} AND ts > {since:DateTime64(3)} AND ts <= {until:DateTime64(3)}
73
74
  ORDER BY ts LIMIT 100`,
74
- { topic, since: formatDateTime(lastTs) }
75
+ { topic, since: formatDateTime(lastTs), until: formatDateTime(time()) }
75
76
  );
76
77
  rows.forEach((row) => {
77
- const timestamp = new Date(row.ts);
78
+ const timestamp = new Date(`${row.ts}Z`);
78
79
  callback({ timestamp, value: row.value, unit });
79
80
  lastTs = timestamp;
80
81
  });
@@ -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
+ }
@@ -31,12 +31,13 @@ export default function scyllaSensor(connection, topic, displayName, unit) {
31
31
  return { timestamp: row.ts, value: row.value, unit };
32
32
  });
33
33
  },
34
- stream(since, step, callback) {
34
+ stream(since, step, callback, clock) {
35
+ const time = clock || (() => { return new Date(); });
35
36
  let lastTs = since;
36
37
  const timer = setInterval(async () => {
37
38
  const rows = await connection.query(
38
- 'SELECT ts, value FROM scada.metrics WHERE topic = ? AND ts > ? LIMIT 100',
39
- [topic, lastTs]
39
+ 'SELECT ts, value FROM scada.metrics WHERE topic = ? AND ts > ? AND ts <= ? LIMIT 100',
40
+ [topic, lastTs, time()]
40
41
  );
41
42
  rows.forEach((row) => {
42
43
  callback({ timestamp: row.ts, value: row.value, unit });
@@ -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
+ }