node-red-contrib-room-tools 1.9.3 → 1.9.5
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/CONTROL_PANEL_AI_GUIDE.md +58 -18
- package/README.md +28 -6
- package/control-bound-protocol.js +25 -7
- package/control-catalog.js +26 -3
- package/control-panel.html +10 -36
- package/control-panel.js +37 -9
- package/control-policy.js +34 -5
- package/control-type-model.js +73 -11
- package/device-availability.js +104 -0
- package/gateway.cjs +36 -12
- package/managed-native-bridge.js +3 -1
- package/meter-frame.js +7 -1
- package/native-binding-plan.js +51 -12
- package/npm-shrinkwrap.json +2 -2
- package/package.json +6 -1
- package/panel-binding-editor.js +311 -62
- package/panel-meter.js +4 -4
- package/panel-momentary.js +7 -1
- package/panel-telemetry-send.js +13 -0
- package/panel-template-instrumentation.js +1 -0
- package/panel-visual-apply.js +86 -27
- package/panel-visual-bindings.js +26 -11
- package/panel-xy-series.js +16 -7
- package/xy-series-frame.js +8 -2
package/native-binding-plan.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { configuredRoutes } = require('./control-bound-protocol')
|
|
2
2
|
const { meterRoute } = require('./meter-route')
|
|
3
3
|
const { xySeriesRoute } = require('./xy-series-route')
|
|
4
|
+
const { targetStates, isMeterTarget } = require('./control-type-model')
|
|
5
|
+
const { availabilityRoute } = require('./device-availability')
|
|
4
6
|
|
|
5
7
|
const supportedTypes = {
|
|
6
8
|
boolean: { eventType: 'Boolean', eventOptions: { boolParamFilter: 'Any' } },
|
|
@@ -32,21 +34,23 @@ function compileNativeBindingPlan ({ panel, targets, scopeId }) {
|
|
|
32
34
|
const writeTargets = new Map()
|
|
33
35
|
const meterStreams = new Map()
|
|
34
36
|
const xySeriesStreams = new Map()
|
|
37
|
+
const healthTargets = new Map()
|
|
35
38
|
const commandRules = []
|
|
36
39
|
const commandWires = []
|
|
37
40
|
|
|
38
|
-
function metadata (device, path, type, access) {
|
|
41
|
+
function metadata (device, path, type, access, adapter) {
|
|
39
42
|
const target = targetIndex.get(JSON.stringify([device, path]))
|
|
40
43
|
if (!target) throw new Error(`Missing target descriptor: ${device} ${path}`)
|
|
41
44
|
if (target.kind !== 'param') throw new Error(`Only parameter targets are supported: ${path}`)
|
|
42
45
|
if (!target.xySeries && !Object.hasOwn(supportedTypes, target.type)) {
|
|
43
46
|
throw new Error(`Unsupported native parameter type: ${path} (${target.type})`)
|
|
44
47
|
}
|
|
45
|
-
if (['mute', 'power'].includes(type) && target
|
|
48
|
+
if (['mute', 'power'].includes(type) && !targetStates(target) &&
|
|
46
49
|
!(target.type === 'integer' && target.booleanEncoding === 'integer01')) {
|
|
47
50
|
throw new Error(`Boolean control bound to ${target.type}: ${path}`)
|
|
48
51
|
}
|
|
49
|
-
if (['level', 'dimmer', 'position'].includes(type) &&
|
|
52
|
+
if (['level', 'dimmer', 'position'].includes(type) &&
|
|
53
|
+
(!['integer', 'float'].includes(target.type) || (adapter === 'range' && isMeterTarget(target)))) {
|
|
50
54
|
throw new Error(`Numeric control bound to ${target.type}: ${path}`)
|
|
51
55
|
}
|
|
52
56
|
if (type === 'text' && !['string', 'dstring'].includes(target.type)) {
|
|
@@ -55,7 +59,7 @@ function compileNativeBindingPlan ({ panel, targets, scopeId }) {
|
|
|
55
59
|
if (['source', 'preset'].includes(type) && !['enum', 'integer', 'string'].includes(target.type)) {
|
|
56
60
|
throw new Error(`Choice control bound to ${target.type}: ${path}`)
|
|
57
61
|
}
|
|
58
|
-
if (type === 'meter' &&
|
|
62
|
+
if (type === 'meter' && !isMeterTarget(target)) {
|
|
59
63
|
throw new Error(`Meter control must bind to a readonly Meter target: ${path}`)
|
|
60
64
|
}
|
|
61
65
|
if (type === 'xy-series' && (!target.xySeries || target.access !== 'ro')) {
|
|
@@ -70,6 +74,34 @@ function compileNativeBindingPlan ({ panel, targets, scopeId }) {
|
|
|
70
74
|
if (typeof target.onebased !== 'boolean') {
|
|
71
75
|
throw new Error(`Target numbering mode is required: ${path}`)
|
|
72
76
|
}
|
|
77
|
+
if (target.availability) {
|
|
78
|
+
for (const point of Object.values(availabilityRoute(target.availability, target.onebased))) {
|
|
79
|
+
const key = JSON.stringify([device, point.path])
|
|
80
|
+
if (healthTargets.has(key)) {
|
|
81
|
+
if (healthTargets.get(key).onebased !== point.onebased) throw Error('Conflicting availability numbering')
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
const index = healthTargets.size + 1
|
|
85
|
+
const statusId = `${scopeId}-health-read-${index}`
|
|
86
|
+
const eventId = `${scopeId}-health-watch-${index}`
|
|
87
|
+
const tagId = `${scopeId}-health-tag-${index}`
|
|
88
|
+
healthTargets.set(key, { statusId, tagId, onebased: point.onebased })
|
|
89
|
+
nodes.push({ id: statusId, type: 'Status', z: subflowId,
|
|
90
|
+
name: `Read availability ${device} ${point.path}`, x: 440, y: 340 + index * 170,
|
|
91
|
+
mojo: { version: '1.0', device: [device], parameter: point.path, onebased: point.onebased }, wires: [[tagId]] })
|
|
92
|
+
nodes.push({ id: eventId, type: 'Event', z: subflowId,
|
|
93
|
+
name: `Watch availability ${device} ${point.path}`, x: 440, y: 400 + index * 170,
|
|
94
|
+
mojo: { version: '1.0', device: [device], event: point.path, eventKind: 'Parameter',
|
|
95
|
+
paramType: 'Boolean', boolParamFilter: 'Any', onebased: point.onebased }, wires: [[tagId]] })
|
|
96
|
+
nodes.push({ id: tagId, type: 'change', z: subflowId,
|
|
97
|
+
name: `Identify availability ${device} ${point.path}`, x: 900, y: 370 + index * 170,
|
|
98
|
+
rules: [
|
|
99
|
+
{ t: 'set', p: 'kind', pt: 'msg', to: 'device-availability', tot: 'str' },
|
|
100
|
+
{ t: 'set', p: 'device', pt: 'msg', to: device, tot: 'str' },
|
|
101
|
+
{ t: 'set', p: 'path', pt: 'msg', to: point.path, tot: 'str' }
|
|
102
|
+
], wires: [[]] })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
73
105
|
return target
|
|
74
106
|
}
|
|
75
107
|
|
|
@@ -77,7 +109,7 @@ function compileNativeBindingPlan ({ panel, targets, scopeId }) {
|
|
|
77
109
|
if (!binding?.device) throw new Error(`Missing device binding: ${id}`)
|
|
78
110
|
if (control.mode !== 'read') {
|
|
79
111
|
if (!binding.writePath) throw new Error(`Missing write path: ${id}`)
|
|
80
|
-
const target = metadata(binding.device, binding.writePath, control.type, 'w')
|
|
112
|
+
const target = metadata(binding.device, binding.writePath, control.type, 'w', control.adapter)
|
|
81
113
|
const key = JSON.stringify([target.device, target.path])
|
|
82
114
|
if (!writeTargets.has(key)) {
|
|
83
115
|
const nodeId = `${scopeId}-write-${writeTargets.size + 1}`
|
|
@@ -97,7 +129,7 @@ function compileNativeBindingPlan ({ panel, targets, scopeId }) {
|
|
|
97
129
|
|
|
98
130
|
if (control.mode !== 'write') {
|
|
99
131
|
if (!binding.readPath) throw new Error(`Missing read path: ${id}`)
|
|
100
|
-
const target = metadata(binding.device, binding.readPath, control.type, 'r')
|
|
132
|
+
const target = metadata(binding.device, binding.readPath, control.type, 'r', control.adapter)
|
|
101
133
|
if (target.meter) {
|
|
102
134
|
const meter = meterRoute(target.meter)
|
|
103
135
|
if (control.mode !== 'read' || control.adapter !== 'meter') throw Error(`Meter is read-only: ${id}`)
|
|
@@ -222,10 +254,14 @@ function compileNativeBindingPlan ({ panel, targets, scopeId }) {
|
|
|
222
254
|
}
|
|
223
255
|
|
|
224
256
|
const commandSwitchId = `${scopeId}-commands`
|
|
225
|
-
// Meter
|
|
257
|
+
// Ordinary feedback, Meter, and XY-series each get a separate vertical band.
|
|
226
258
|
for (const node of nodes) {
|
|
259
|
+
if (node.id.startsWith(`${scopeId}-health-`)) {
|
|
260
|
+
node.y += readTargets.size * 170 + (meterStreams.size + xySeriesStreams.size) * 280
|
|
261
|
+
}
|
|
227
262
|
if (node.id.startsWith(`${scopeId}-meter-`) || node.id.startsWith(`${scopeId}-xy-series-`)) {
|
|
228
263
|
node.y += readTargets.size * 170
|
|
264
|
+
if (node.id.startsWith(`${scopeId}-xy-series-`)) node.y += meterStreams.size * 280
|
|
229
265
|
}
|
|
230
266
|
}
|
|
231
267
|
const routerId = `${scopeId}-router`
|
|
@@ -248,18 +284,20 @@ function compileNativeBindingPlan ({ panel, targets, scopeId }) {
|
|
|
248
284
|
],
|
|
249
285
|
checkall: 'false', repair: false, outputs: 2,
|
|
250
286
|
wires: [commandRules.length ? [commandSwitchId] : [],
|
|
251
|
-
[...readTargets.values(), ...meterStreams.values(), ...xySeriesStreams.values()].map(target => target.statusId)]
|
|
287
|
+
[...readTargets.values(), ...meterStreams.values(), ...xySeriesStreams.values(), ...healthTargets.values()].map(target => target.statusId)]
|
|
252
288
|
})
|
|
253
289
|
const subflow = {
|
|
254
290
|
id: subflowId, type: 'subflow', name: `Binding bridge ${scopeId}`,
|
|
255
291
|
info: 'Generated panel binding bridge. Requires a Controller node and authenticated command source.',
|
|
256
292
|
category: 'panel binding',
|
|
257
293
|
in: [{ x: 170, y: 150, wires: [{ id: routerId }] }],
|
|
258
|
-
out: readTargets.size || meterStreams.size || xySeriesStreams.size
|
|
259
|
-
? [{ x: meterStreams.size ? 1600 : 1290,
|
|
294
|
+
out: readTargets.size || meterStreams.size || xySeriesStreams.size || healthTargets.size
|
|
295
|
+
? [{ x: meterStreams.size || xySeriesStreams.size ? 1600 : 1290,
|
|
296
|
+
y: Math.round((390 + (readTargets.size + meterStreams.size + xySeriesStreams.size + 1) * 85) / 10) * 10,
|
|
260
297
|
wires: [...readTargets.values()].map(target => ({ id: target.tagId, port: 0 }))
|
|
261
298
|
.concat([...meterStreams.values()].flatMap(target => target.tagIds.map(id => ({ id, port: 0 }))))
|
|
262
|
-
.concat([...xySeriesStreams.values()].flatMap(target => target.tagIds.map(id => ({ id, port: 0 }))))
|
|
299
|
+
.concat([...xySeriesStreams.values()].flatMap(target => target.tagIds.map(id => ({ id, port: 0 }))))
|
|
300
|
+
.concat([...healthTargets.values()].map(target => ({ id: target.tagId, port: 0 }))) }]
|
|
263
301
|
: [],
|
|
264
302
|
env: [], meta: {}, color: '#96bca7'
|
|
265
303
|
}
|
|
@@ -268,7 +306,8 @@ function compileNativeBindingPlan ({ panel, targets, scopeId }) {
|
|
|
268
306
|
manifest: {
|
|
269
307
|
subflowId,
|
|
270
308
|
controls: routes.size,
|
|
271
|
-
nativeReads: readTargets.size + meterStreams.size + xySeriesStreams.size,
|
|
309
|
+
nativeReads: readTargets.size + meterStreams.size + xySeriesStreams.size + healthTargets.size,
|
|
310
|
+
...(healthTargets.size ? { nativeAvailabilityReads: healthTargets.size } : {}),
|
|
272
311
|
nativeMeterStreams: meterStreams.size,
|
|
273
312
|
nativeXYSeriesStreams: xySeriesStreams.size,
|
|
274
313
|
nativeWrites: writeTargets.size,
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-room-tools",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"lockfileVersion": 2,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "node-red-contrib-room-tools",
|
|
9
|
-
"version": "1.9.
|
|
9
|
+
"version": "1.9.5",
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@vue/compiler-sfc": "3.5.42",
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-room-tools",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"description": "Authenticated visual control panel binding for Node-RED and MUSE Automator",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test test-*.cjs"
|
|
8
|
+
},
|
|
6
9
|
"engines": {
|
|
7
10
|
"node": ">=18.16.0"
|
|
8
11
|
},
|
|
@@ -23,9 +26,11 @@
|
|
|
23
26
|
"control-catalog.js",
|
|
24
27
|
"control-type-contract.json",
|
|
25
28
|
"control-type-model.js",
|
|
29
|
+
"device-availability.js",
|
|
26
30
|
"meter-route.js",
|
|
27
31
|
"meter-frame.js",
|
|
28
32
|
"panel-meter.js",
|
|
33
|
+
"panel-telemetry-send.js",
|
|
29
34
|
"panel-meter-client.js",
|
|
30
35
|
"xy-series-route.js",
|
|
31
36
|
"xy-series-frame.js",
|