node-red-dashboard-2-hmi 0.1.10 → 0.1.11

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/nodes/alarms.js CHANGED
@@ -1,177 +1,177 @@
1
- 'use strict'
2
-
3
- // Alarm list for one canvas (ISA-18.2 style): every alarm the symbols can show
4
- // (fault key, limits, alarm banners, tripped breakers) is tracked with its
5
- // activation time and acknowledgement. An alarm leaves the active list only when
6
- // it is back to normal AND acknowledged; every change goes to the history.
7
-
8
- const HISTORY_MAX = 500
9
-
10
- const num = v => (v === '' || v === undefined || v === null || typeof v === 'boolean' || !isFinite(Number(v))) ? null : Number(v)
11
-
12
- function isOn (value, rule, threshold) {
13
- if (value === undefined || value === null || value === '') return null
14
- if (rule === 'gt') return Number(value) > Number(threshold)
15
- if (typeof value === 'boolean') return value
16
- if (typeof value === 'string' && isNaN(Number(value))) return value.length > 0
17
- return Number(value) !== 0
18
- }
19
-
20
- function onState (value, p) {
21
- const on = isOn(value, p && p.onWhen, p && p.threshold)
22
- return on !== null && p && p.invert === 'yes' ? !on : on
23
- }
24
-
25
- // same as scaleValue in ui/symbols: raw × factor ÷ divisor + offset
26
- function scaled (sym, raw) {
27
- const sc = sym.scale
28
- const n = num(raw)
29
- if (n === null || !sc) return n
30
- const k = num(sc.factor)
31
- const d = num(sc.divisor)
32
- const o = num(sc.offset)
33
- return Math.round((n * (k === null ? 1 : k) / (d === null || d === 0 ? 1 : d) + (o === null ? 0 : o)) * 1e9) / 1e9
34
- }
35
-
36
- function nameOf (sym) {
37
- const p = sym.props || {}
38
- return String(p.label || p.title || p.text || sym.key || sym.type)
39
- }
40
-
41
- const RANGE_TYPES = ['tank', 'gauge', 'bar', 'tag', 'bagFilter', 'oven', 'flowmeter', 'instrument', 'scale', 'isaBar']
42
-
43
- // Every alarm the canvas can raise: { id, symbolId, text, priority, active(values) }
44
- function alarmDefs (symbols) {
45
- const out = []
46
- for (const s of Array.isArray(symbols) ? symbols : []) {
47
- if (!s || typeof s !== 'object') continue
48
- const p = s.props || {}
49
- if (p.alarmList === 'no') continue
50
- const name = nameOf(s)
51
- const prio = String(p.priority || '2')
52
- if (p.faultKey) {
53
- out.push({ id: s.id + ':fault', symbolId: s.id, text: name + ' em falha', priority: String(p.priority || '1'), active: v => isOn(v[p.faultKey], 'nonzero') === true })
54
- }
55
- if (s.type === 'breaker' && p.tripKey) {
56
- out.push({ id: s.id + ':trip', symbolId: s.id, text: name + ' disparado', priority: String(p.priority || '1'), active: v => isOn(v[p.tripKey], 'nonzero') === true })
57
- }
58
- if (s.type === 'alarm' && s.key) {
59
- out.push({ id: s.id + ':on', symbolId: s.id, text: String(p.text || name), priority: String(p.priority || '1'), active: v => onState(v[s.key], p) === true })
60
- }
61
- if (s.key && RANGE_TYPES.includes(s.type)) {
62
- const val = v => scaled(s, v[s.key])
63
- const lim = (k, label, pr, test) => {
64
- const l = num(p[k])
65
- if (l !== null) out.push({ id: s.id + ':' + k, symbolId: s.id, text: name + ' ' + label, priority: pr, active: v => { const x = val(v); return x !== null && test(x, l) } })
66
- }
67
- if (s.type === 'isaBar') {
68
- lim('lolo', 'muito baixo', '1', (x, l) => x < l)
69
- lim('lo', 'baixo', prio, (x, l) => x < l && !(num(p.lolo) !== null && x < num(p.lolo)))
70
- lim('hi', 'alto', prio, (x, l) => x > l && !(num(p.hihi) !== null && x > num(p.hihi)))
71
- lim('hihi', 'muito alto', '1', (x, l) => x > l)
72
- } else {
73
- lim('lo', 'baixo', prio, (x, l) => x < l)
74
- lim('hi', 'alto', prio, (x, l) => x > l)
75
- }
76
- }
77
- }
78
- return out
79
- }
80
-
81
- function createAlarms (symbols, saved) {
82
- const defs = alarmDefs(symbols)
83
- const known = new Set(defs.map(d => d.id))
84
- const byId = new Map(defs.map(d => [d.id, d]))
85
- // shelved (suppressed) alarms: id -> { id, symbolId, text, priority, until, by }
86
- const shelved = new Map()
87
- // id -> { id, symbolId, text, priority, active, acked, since, cleared, ackBy }
88
- const list = new Map()
89
- let history = []
90
- if (saved && typeof saved === 'object') {
91
- // text and priority follow the symbol as it is now (renamed after a deploy)
92
- const fresh = a => ({ ...a, text: byId.get(a.id).text, priority: byId.get(a.id).priority })
93
- for (const a of Array.isArray(saved.active) ? saved.active : []) if (a && known.has(a.id)) list.set(a.id, fresh(a))
94
- if (Array.isArray(saved.history)) history = saved.history.slice(0, HISTORY_MAX)
95
- for (const sh of Array.isArray(saved.shelved) ? saved.shelved : []) if (sh && known.has(sh.id)) shelved.set(sh.id, fresh(sh))
96
- }
97
- // goes up on every history row, so pages only get the history when it changed
98
- let version = 0
99
- const log = (a, event, now, user) => {
100
- version++
101
- history.unshift({ ts: now, id: a.id, symbolId: a.symbolId, text: a.text, priority: a.priority, event, ...(user ? { user } : {}) })
102
- if (history.length > HISTORY_MAX) history.length = HISTORY_MAX
103
- return { event, id: a.id, symbolId: a.symbolId, text: a.text, priority: a.priority, ts: now, ...(user ? { user } : {}) }
104
- }
105
-
106
- return {
107
- count: defs.length,
108
- get version () { return version },
109
- has (id) { return byId.has(id) },
110
- // checks every alarm against the values; returns the events (ATIVO, NORMAL)
111
- evaluate (values, now) {
112
- const events = []
113
- for (const d of defs) {
114
- let on
115
- try { on = d.active(values || {}) } catch (e) { on = false }
116
- const cur = list.get(d.id)
117
- if (on && (!cur || !cur.active)) {
118
- const a = { id: d.id, symbolId: d.symbolId, text: d.text, priority: d.priority, active: true, acked: false, since: now }
119
- list.set(d.id, a)
120
- events.push(log(a, 'ATIVO', now))
121
- } else if (!on && cur && cur.active) {
122
- cur.active = false
123
- cur.cleared = now
124
- events.push(log(cur, 'NORMAL', now))
125
- if (cur.acked) list.delete(d.id)
126
- }
127
- }
128
- return events
129
- },
130
- // acknowledges one alarm (by id), all alarms of a symbol (symbol:<id>) or every alarm ('all')
131
- ack (which, now, user) {
132
- const events = []
133
- for (const a of [...list.values()]) {
134
- // "all" is what the operator sees: shelved alarms are not in it
135
- const match = (which === 'all' && !shelved.has(a.id)) || a.id === which || (typeof which === 'string' && which.startsWith('symbol:') && a.symbolId === which.slice(7))
136
- if (!match || a.acked) continue
137
- a.acked = true
138
- a.ackTs = now
139
- if (user) a.ackBy = user
140
- events.push(log(a, 'RECONHECIDO', now, user))
141
- if (!a.active) list.delete(a.id)
142
- }
143
- return events
144
- },
145
- // suppresses an alarm for some minutes (ISA-18.2 shelving): it leaves the list and the counters
146
- shelve (id, minutes, now, user) {
147
- const d = byId.get(id)
148
- if (!d) return []
149
- const min = Math.max(1, Math.min(1440, Number(minutes) || 30))
150
- const sh = { id, symbolId: d.symbolId, text: d.text, priority: d.priority, until: now + min * 60000, ...(user ? { by: user } : {}) }
151
- shelved.set(id, sh)
152
- return [log(sh, 'SUPRIMIDO', now, user)]
153
- },
154
- unshelve (id, now, user) {
155
- const sh = shelved.get(id)
156
- if (!sh) return []
157
- shelved.delete(id)
158
- return [log(sh, 'LIBERADO', now, user)]
159
- },
160
- // shelved alarms whose time is over come back
161
- expire (now) {
162
- const events = []
163
- for (const sh of [...shelved.values()]) if (sh.until <= now) events.push(...this.unshelve(sh.id, now, ''))
164
- return events
165
- },
166
- snapshot (historyRows) {
167
- const order = { 1: 0, 2: 1, 3: 2 }
168
- const active = [...list.values()].filter(a => !shelved.has(a.id)).sort((a, b) => (a.acked - b.acked) || ((order[a.priority] ?? 3) - (order[b.priority] ?? 3)) || (b.since - a.since))
169
- return { active, history: history.slice(0, historyRows === undefined ? 100 : historyRows), unacked: active.filter(a => !a.acked).length, shelved: [...shelved.values()].sort((a, b) => a.until - b.until) }
170
- },
171
- save () {
172
- return { active: [...list.values()], history, shelved: [...shelved.values()] }
173
- }
174
- }
175
- }
176
-
177
- module.exports = { alarmDefs, createAlarms, onState }
1
+ 'use strict'
2
+
3
+ // Alarm list for one canvas (ISA-18.2 style): every alarm the symbols can show
4
+ // (fault key, limits, alarm banners, tripped breakers) is tracked with its
5
+ // activation time and acknowledgement. An alarm leaves the active list only when
6
+ // it is back to normal AND acknowledged; every change goes to the history.
7
+
8
+ const HISTORY_MAX = 500
9
+
10
+ const num = v => (v === '' || v === undefined || v === null || typeof v === 'boolean' || !isFinite(Number(v))) ? null : Number(v)
11
+
12
+ function isOn (value, rule, threshold) {
13
+ if (value === undefined || value === null || value === '') return null
14
+ if (rule === 'gt') return Number(value) > Number(threshold)
15
+ if (typeof value === 'boolean') return value
16
+ if (typeof value === 'string' && isNaN(Number(value))) return value.length > 0
17
+ return Number(value) !== 0
18
+ }
19
+
20
+ function onState (value, p) {
21
+ const on = isOn(value, p && p.onWhen, p && p.threshold)
22
+ return on !== null && p && p.invert === 'yes' ? !on : on
23
+ }
24
+
25
+ // same as scaleValue in ui/symbols: raw × factor ÷ divisor + offset
26
+ function scaled (sym, raw) {
27
+ const sc = sym.scale
28
+ const n = num(raw)
29
+ if (n === null || !sc) return n
30
+ const k = num(sc.factor)
31
+ const d = num(sc.divisor)
32
+ const o = num(sc.offset)
33
+ return Math.round((n * (k === null ? 1 : k) / (d === null || d === 0 ? 1 : d) + (o === null ? 0 : o)) * 1e9) / 1e9
34
+ }
35
+
36
+ function nameOf (sym) {
37
+ const p = sym.props || {}
38
+ return String(p.label || p.title || p.text || sym.key || sym.type)
39
+ }
40
+
41
+ const RANGE_TYPES = ['tank', 'gauge', 'bar', 'tag', 'bagFilter', 'oven', 'flowmeter', 'instrument', 'scale', 'isaBar']
42
+
43
+ // Every alarm the canvas can raise: { id, symbolId, text, priority, active(values) }
44
+ function alarmDefs (symbols) {
45
+ const out = []
46
+ for (const s of Array.isArray(symbols) ? symbols : []) {
47
+ if (!s || typeof s !== 'object') continue
48
+ const p = s.props || {}
49
+ if (p.alarmList === 'no') continue
50
+ const name = nameOf(s)
51
+ const prio = String(p.priority || '2')
52
+ if (p.faultKey) {
53
+ out.push({ id: s.id + ':fault', symbolId: s.id, text: name + ' em falha', priority: String(p.priority || '1'), active: v => isOn(v[p.faultKey], 'nonzero') === true })
54
+ }
55
+ if (s.type === 'breaker' && p.tripKey) {
56
+ out.push({ id: s.id + ':trip', symbolId: s.id, text: name + ' disparado', priority: String(p.priority || '1'), active: v => isOn(v[p.tripKey], 'nonzero') === true })
57
+ }
58
+ if (s.type === 'alarm' && s.key) {
59
+ out.push({ id: s.id + ':on', symbolId: s.id, text: String(p.text || name), priority: String(p.priority || '1'), active: v => onState(v[s.key], p) === true })
60
+ }
61
+ if (s.key && RANGE_TYPES.includes(s.type)) {
62
+ const val = v => scaled(s, v[s.key])
63
+ const lim = (k, label, pr, test) => {
64
+ const l = num(p[k])
65
+ if (l !== null) out.push({ id: s.id + ':' + k, symbolId: s.id, text: name + ' ' + label, priority: pr, active: v => { const x = val(v); return x !== null && test(x, l) } })
66
+ }
67
+ if (s.type === 'isaBar') {
68
+ lim('lolo', 'muito baixo', '1', (x, l) => x < l)
69
+ lim('lo', 'baixo', prio, (x, l) => x < l && !(num(p.lolo) !== null && x < num(p.lolo)))
70
+ lim('hi', 'alto', prio, (x, l) => x > l && !(num(p.hihi) !== null && x > num(p.hihi)))
71
+ lim('hihi', 'muito alto', '1', (x, l) => x > l)
72
+ } else {
73
+ lim('lo', 'baixo', prio, (x, l) => x < l)
74
+ lim('hi', 'alto', prio, (x, l) => x > l)
75
+ }
76
+ }
77
+ }
78
+ return out
79
+ }
80
+
81
+ function createAlarms (symbols, saved) {
82
+ const defs = alarmDefs(symbols)
83
+ const known = new Set(defs.map(d => d.id))
84
+ const byId = new Map(defs.map(d => [d.id, d]))
85
+ // shelved (suppressed) alarms: id -> { id, symbolId, text, priority, until, by }
86
+ const shelved = new Map()
87
+ // id -> { id, symbolId, text, priority, active, acked, since, cleared, ackBy }
88
+ const list = new Map()
89
+ let history = []
90
+ if (saved && typeof saved === 'object') {
91
+ // text and priority follow the symbol as it is now (renamed after a deploy)
92
+ const fresh = a => ({ ...a, text: byId.get(a.id).text, priority: byId.get(a.id).priority })
93
+ for (const a of Array.isArray(saved.active) ? saved.active : []) if (a && known.has(a.id)) list.set(a.id, fresh(a))
94
+ if (Array.isArray(saved.history)) history = saved.history.slice(0, HISTORY_MAX)
95
+ for (const sh of Array.isArray(saved.shelved) ? saved.shelved : []) if (sh && known.has(sh.id)) shelved.set(sh.id, fresh(sh))
96
+ }
97
+ // goes up on every history row, so pages only get the history when it changed
98
+ let version = 0
99
+ const log = (a, event, now, user) => {
100
+ version++
101
+ history.unshift({ ts: now, id: a.id, symbolId: a.symbolId, text: a.text, priority: a.priority, event, ...(user ? { user } : {}) })
102
+ if (history.length > HISTORY_MAX) history.length = HISTORY_MAX
103
+ return { event, id: a.id, symbolId: a.symbolId, text: a.text, priority: a.priority, ts: now, ...(user ? { user } : {}) }
104
+ }
105
+
106
+ return {
107
+ count: defs.length,
108
+ get version () { return version },
109
+ has (id) { return byId.has(id) },
110
+ // checks every alarm against the values; returns the events (ATIVO, NORMAL)
111
+ evaluate (values, now) {
112
+ const events = []
113
+ for (const d of defs) {
114
+ let on
115
+ try { on = d.active(values || {}) } catch (e) { on = false }
116
+ const cur = list.get(d.id)
117
+ if (on && (!cur || !cur.active)) {
118
+ const a = { id: d.id, symbolId: d.symbolId, text: d.text, priority: d.priority, active: true, acked: false, since: now }
119
+ list.set(d.id, a)
120
+ events.push(log(a, 'ATIVO', now))
121
+ } else if (!on && cur && cur.active) {
122
+ cur.active = false
123
+ cur.cleared = now
124
+ events.push(log(cur, 'NORMAL', now))
125
+ if (cur.acked) list.delete(d.id)
126
+ }
127
+ }
128
+ return events
129
+ },
130
+ // acknowledges one alarm (by id), all alarms of a symbol (symbol:<id>) or every alarm ('all')
131
+ ack (which, now, user) {
132
+ const events = []
133
+ for (const a of [...list.values()]) {
134
+ // "all" is what the operator sees: shelved alarms are not in it
135
+ const match = (which === 'all' && !shelved.has(a.id)) || a.id === which || (typeof which === 'string' && which.startsWith('symbol:') && a.symbolId === which.slice(7))
136
+ if (!match || a.acked) continue
137
+ a.acked = true
138
+ a.ackTs = now
139
+ if (user) a.ackBy = user
140
+ events.push(log(a, 'RECONHECIDO', now, user))
141
+ if (!a.active) list.delete(a.id)
142
+ }
143
+ return events
144
+ },
145
+ // suppresses an alarm for some minutes (ISA-18.2 shelving): it leaves the list and the counters
146
+ shelve (id, minutes, now, user) {
147
+ const d = byId.get(id)
148
+ if (!d) return []
149
+ const min = Math.max(1, Math.min(1440, Number(minutes) || 30))
150
+ const sh = { id, symbolId: d.symbolId, text: d.text, priority: d.priority, until: now + min * 60000, ...(user ? { by: user } : {}) }
151
+ shelved.set(id, sh)
152
+ return [log(sh, 'SUPRIMIDO', now, user)]
153
+ },
154
+ unshelve (id, now, user) {
155
+ const sh = shelved.get(id)
156
+ if (!sh) return []
157
+ shelved.delete(id)
158
+ return [log(sh, 'LIBERADO', now, user)]
159
+ },
160
+ // shelved alarms whose time is over come back
161
+ expire (now) {
162
+ const events = []
163
+ for (const sh of [...shelved.values()]) if (sh.until <= now) events.push(...this.unshelve(sh.id, now, ''))
164
+ return events
165
+ },
166
+ snapshot (historyRows) {
167
+ const order = { 1: 0, 2: 1, 3: 2 }
168
+ const active = [...list.values()].filter(a => !shelved.has(a.id)).sort((a, b) => (a.acked - b.acked) || ((order[a.priority] ?? 3) - (order[b.priority] ?? 3)) || (b.since - a.since))
169
+ return { active, history: history.slice(0, historyRows === undefined ? 100 : historyRows), unacked: active.filter(a => !a.acked).length, shelved: [...shelved.values()].sort((a, b) => a.until - b.until) }
170
+ },
171
+ save () {
172
+ return { active: [...list.values()], history, shelved: [...shelved.values()] }
173
+ }
174
+ }
175
+ }
176
+
177
+ module.exports = { alarmDefs, createAlarms, onState }
package/nodes/clients.js CHANGED
@@ -1,31 +1,31 @@
1
- 'use strict'
2
-
3
- // Dashboard clicks on a canvas never go through the widget-action path of
4
- // Dashboard 2.0 (that one also feeds beforeSend, so a page could fake values).
5
- // The plugin in plugin.js hands them here with the socket, and each canvas
6
- // node answers its own: canvas id -> { action(conn, msg), load(conn, msg) }
7
- const canvases = new Map()
8
-
9
- // who is on the other end of a socket: login from FlowFuse / multi-user plugins,
10
- // or the user of HTTP basic auth (httpNodeAuth) in front of the dashboard
11
- function clientOf (RED, conn) {
12
- let item = { _client: {} }
13
- try {
14
- for (const p of RED.plugins.getByType('node-red-dashboard-2')) {
15
- if (p.hooks && typeof p.hooks.onAddConnectionCredentials === 'function') item = p.hooks.onAddConnectionCredentials(conn, item) || item
16
- }
17
- } catch (e) {}
18
- const hs = (conn && conn.handshake) || {}
19
- const c = { ...(item._client || {}), socketId: conn && conn.id, socketIp: hs.address, clientId: hs.query && hs.query.clientId }
20
- if (!c.user) {
21
- const auth = String((hs.headers && hs.headers.authorization) || '')
22
- if (/^basic /i.test(auth)) {
23
- const plain = Buffer.from(auth.slice(6).trim(), 'base64').toString('utf8')
24
- const name = plain.slice(0, Math.max(0, plain.indexOf(':')))
25
- if (name) c.user = { username: name }
26
- }
27
- }
28
- return c
29
- }
30
-
31
- module.exports = { canvases, clientOf }
1
+ 'use strict'
2
+
3
+ // Dashboard clicks on a canvas never go through the widget-action path of
4
+ // Dashboard 2.0 (that one also feeds beforeSend, so a page could fake values).
5
+ // The plugin in plugin.js hands them here with the socket, and each canvas
6
+ // node answers its own: canvas id -> { action(conn, msg), load(conn, msg) }
7
+ const canvases = new Map()
8
+
9
+ // who is on the other end of a socket: login from FlowFuse / multi-user plugins,
10
+ // or the user of HTTP basic auth (httpNodeAuth) in front of the dashboard
11
+ function clientOf (RED, conn) {
12
+ let item = { _client: {} }
13
+ try {
14
+ for (const p of RED.plugins.getByType('node-red-dashboard-2')) {
15
+ if (p.hooks && typeof p.hooks.onAddConnectionCredentials === 'function') item = p.hooks.onAddConnectionCredentials(conn, item) || item
16
+ }
17
+ } catch (e) {}
18
+ const hs = (conn && conn.handshake) || {}
19
+ const c = { ...(item._client || {}), socketId: conn && conn.id, socketIp: hs.address, clientId: hs.query && hs.query.clientId }
20
+ if (!c.user) {
21
+ const auth = String((hs.headers && hs.headers.authorization) || '')
22
+ if (/^basic /i.test(auth)) {
23
+ const plain = Buffer.from(auth.slice(6).trim(), 'base64').toString('utf8')
24
+ const name = plain.slice(0, Math.max(0, plain.indexOf(':')))
25
+ if (name) c.user = { username: name }
26
+ }
27
+ }
28
+ return c
29
+ }
30
+
31
+ module.exports = { canvases, clientOf }
package/nodes/plugin.js CHANGED
@@ -1,24 +1,24 @@
1
- 'use strict'
2
-
3
- const { canvases } = require('./clients')
4
-
5
- // Dashboard 2.0 plugin: clicks and page loads of an hmi canvas go to the canvas
6
- // node itself (with the socket), never to the generic widget-action path
7
- module.exports = function (RED) {
8
- RED.plugins.registerPlugin('node-red-dashboard-2-hmi', {
9
- type: 'node-red-dashboard-2',
10
- hooks: {
11
- onAction (conn, id, msg) {
12
- const c = canvases.get(id)
13
- if (!c) return msg
14
- try { c.action(conn, msg) } catch (e) { c.error(e) }
15
- return null
16
- },
17
- onLoad (conn, id, msg, state) {
18
- const c = canvases.get(id)
19
- if (!c) return msg
20
- try { return c.load(conn, msg) } catch (e) { c.error(e); return msg }
21
- }
22
- }
23
- })
24
- }
1
+ 'use strict'
2
+
3
+ const { canvases } = require('./clients')
4
+
5
+ // Dashboard 2.0 plugin: clicks and page loads of an hmi canvas go to the canvas
6
+ // node itself (with the socket), never to the generic widget-action path
7
+ module.exports = function (RED) {
8
+ RED.plugins.registerPlugin('node-red-dashboard-2-hmi', {
9
+ type: 'node-red-dashboard-2',
10
+ hooks: {
11
+ onAction (conn, id, msg) {
12
+ const c = canvases.get(id)
13
+ if (!c) return msg
14
+ try { c.action(conn, msg) } catch (e) { c.error(e) }
15
+ return null
16
+ },
17
+ onLoad (conn, id, msg, state) {
18
+ const c = canvases.get(id)
19
+ if (!c) return msg
20
+ try { return c.load(conn, msg) } catch (e) { c.error(e); return msg }
21
+ }
22
+ }
23
+ })
24
+ }
package/nodes/trend.js CHANGED
@@ -1,54 +1,54 @@
1
- 'use strict'
2
-
3
- // Recent samples of every key shown in a trend symbol: key -> [[time ms, value], ...]
4
- // A window holds at most POINTS samples spread over time: faster data is thinned
5
- // (one sample per window / POINTS), so a 1 h trend really covers the hour.
6
- // The page (UIHmiCanvas.vue) keeps its own samples the same way.
7
- const POINTS = 1500
8
-
9
- function windowsOf (symbols) {
10
- const windows = {}
11
- for (const s of Array.isArray(symbols) ? symbols : []) {
12
- if (!s || s.type !== 'trend' || !s.key) continue
13
- const win = Math.max(5, Math.min(3600, Number(s.props && s.props.window) || 60)) * 1000
14
- windows[s.key] = Math.max(windows[s.key] || 0, win)
15
- }
16
- return windows
17
- }
18
-
19
- // adds one sample to a list, dropping old ones; returns the new list
20
- function addSample (list, now, value, win) {
21
- const out = list.filter(q => q[0] >= now - win)
22
- const step = win / POINTS
23
- const last = out[out.length - 1]
24
- if (last && now - last[0] < step && out.length > 1 && now - out[out.length - 2][0] < step) out[out.length - 1] = [now, value]
25
- else out.push([now, value])
26
- return out.length > POINTS + 2 ? out.slice(-(POINTS + 2)) : out
27
- }
28
-
29
- // saved: the history of the previous instance (kept across a deploy)
30
- function createTrendHistory (symbols, saved) {
31
- const windows = windowsOf(symbols)
32
- const hist = {}
33
- if (saved && typeof saved === 'object') {
34
- for (const k of Object.keys(windows)) if (Array.isArray(saved[k])) hist[k] = saved[k].slice(-(POINTS + 2))
35
- }
36
- return {
37
- size: Object.keys(windows).length,
38
- record (values, now) {
39
- for (const k of Object.keys(windows)) {
40
- const raw = values ? values[k] : undefined
41
- const v = Number(raw)
42
- const ok = raw !== undefined && raw !== null && raw !== '' && typeof raw !== 'boolean' && isFinite(v)
43
- hist[k] = ok ? addSample(hist[k] || [], now, v, windows[k]) : (hist[k] || []).filter(q => q[0] >= now - windows[k])
44
- }
45
- },
46
- snapshot () {
47
- const out = {}
48
- for (const k of Object.keys(hist)) out[k] = hist[k].slice()
49
- return out
50
- }
51
- }
52
- }
53
-
54
- module.exports = { createTrendHistory, addSample, POINTS }
1
+ 'use strict'
2
+
3
+ // Recent samples of every key shown in a trend symbol: key -> [[time ms, value], ...]
4
+ // A window holds at most POINTS samples spread over time: faster data is thinned
5
+ // (one sample per window / POINTS), so a 1 h trend really covers the hour.
6
+ // The page (UIHmiCanvas.vue) keeps its own samples the same way.
7
+ const POINTS = 1500
8
+
9
+ function windowsOf (symbols) {
10
+ const windows = {}
11
+ for (const s of Array.isArray(symbols) ? symbols : []) {
12
+ if (!s || s.type !== 'trend' || !s.key) continue
13
+ const win = Math.max(5, Math.min(3600, Number(s.props && s.props.window) || 60)) * 1000
14
+ windows[s.key] = Math.max(windows[s.key] || 0, win)
15
+ }
16
+ return windows
17
+ }
18
+
19
+ // adds one sample to a list, dropping old ones; returns the new list
20
+ function addSample (list, now, value, win) {
21
+ const out = list.filter(q => q[0] >= now - win)
22
+ const step = win / POINTS
23
+ const last = out[out.length - 1]
24
+ if (last && now - last[0] < step && out.length > 1 && now - out[out.length - 2][0] < step) out[out.length - 1] = [now, value]
25
+ else out.push([now, value])
26
+ return out.length > POINTS + 2 ? out.slice(-(POINTS + 2)) : out
27
+ }
28
+
29
+ // saved: the history of the previous instance (kept across a deploy)
30
+ function createTrendHistory (symbols, saved) {
31
+ const windows = windowsOf(symbols)
32
+ const hist = {}
33
+ if (saved && typeof saved === 'object') {
34
+ for (const k of Object.keys(windows)) if (Array.isArray(saved[k])) hist[k] = saved[k].slice(-(POINTS + 2))
35
+ }
36
+ return {
37
+ size: Object.keys(windows).length,
38
+ record (values, now) {
39
+ for (const k of Object.keys(windows)) {
40
+ const raw = values ? values[k] : undefined
41
+ const v = Number(raw)
42
+ const ok = raw !== undefined && raw !== null && raw !== '' && typeof raw !== 'boolean' && isFinite(v)
43
+ hist[k] = ok ? addSample(hist[k] || [], now, v, windows[k]) : (hist[k] || []).filter(q => q[0] >= now - windows[k])
44
+ }
45
+ },
46
+ snapshot () {
47
+ const out = {}
48
+ for (const k of Object.keys(hist)) out[k] = hist[k].slice()
49
+ return out
50
+ }
51
+ }
52
+ }
53
+
54
+ module.exports = { createTrendHistory, addSample, POINTS }