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/CHANGELOG.md +144 -140
- package/LICENSE +21 -21
- package/README.md +273 -273
- package/docs/mensagens.md +266 -266
- package/examples/clp-schneider.json +431 -431
- package/examples/envase.json +545 -545
- package/examples/esteira.json +639 -639
- package/examples/pintura.json +610 -610
- package/examples/tela-basica.json +572 -572
- package/nodes/access.js +69 -69
- package/nodes/admin.js +170 -166
- package/nodes/alarms.js +177 -177
- package/nodes/clients.js +31 -31
- package/nodes/plugin.js +24 -24
- package/nodes/trend.js +54 -54
- package/nodes/ui-hmi-canvas.html +383 -383
- package/nodes/ui-hmi-canvas.js +335 -335
- package/nodes/values.js +176 -176
- package/package.json +63 -63
- package/resources/hmi-editor.js +10893 -10887
- package/resources/ui-hmi-canvas.umd.js +667 -667
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 }
|