node-red-contrib-room-tools 1.9.4 → 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 +54 -16
- package/README.md +19 -3
- package/control-bound-protocol.js +23 -6
- package/control-catalog.js +26 -3
- package/control-panel.js +29 -3
- package/control-policy.js +34 -5
- package/control-type-model.js +68 -8
- package/device-availability.js +104 -0
- package/gateway.cjs +13 -1
- package/managed-native-bridge.js +3 -1
- package/meter-frame.js +7 -1
- package/native-binding-plan.js +47 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/panel-binding-editor.js +208 -30
- package/panel-momentary.js +7 -1
- package/panel-template-instrumentation.js +1 -0
- package/panel-visual-apply.js +86 -27
- package/panel-visual-bindings.js +18 -4
- package/xy-series-frame.js +7 -1
package/panel-visual-bindings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { inspectTemplate } = require('./panel-template-instrumentation')
|
|
2
|
-
const { compatibleTarget, controlTypeForAdapter, targetMeta, bindingModes } = require('./control-type-model')
|
|
2
|
+
const { compatibleTarget, controlTypeForAdapter, targetMeta, bindingModes, switchMapping, stateReverse, isMeterTarget } = require('./control-type-model')
|
|
3
3
|
const { meterRoute } = require('./meter-route')
|
|
4
4
|
const { xySeriesRoute } = require('./xy-series-route')
|
|
5
5
|
|
|
@@ -58,6 +58,16 @@ function visualRoutes (config) {
|
|
|
58
58
|
if (!bindingModes(entry.adapter).includes(entry.mode) || !entry.type || !entry.label) {
|
|
59
59
|
throw new Error(`Invalid visual binding settings: ${id}`)
|
|
60
60
|
}
|
|
61
|
+
if (entry.adapter === 'meter' && (entry.type !== 'meter' || entry.mode !== 'read' ||
|
|
62
|
+
!entry.readPath || entry.writePath || entry.writeMeta || !entry.readMeta || !isMeterTarget(entry.readMeta))) {
|
|
63
|
+
throw Error(`Invalid readonly meter binding: ${id}`)
|
|
64
|
+
}
|
|
65
|
+
if (entry.adapter === 'range' && [entry.writeMeta, entry.readMeta].some(meta => meta && isMeterTarget(meta))) {
|
|
66
|
+
throw Error(`Numeric controls cannot bind to readonly meter targets: ${id}`)
|
|
67
|
+
}
|
|
68
|
+
for (const meta of [entry.readMeta, entry.writeMeta]) {
|
|
69
|
+
if (meta?.availability) require('./device-availability').availabilityRoute(meta.availability)
|
|
70
|
+
}
|
|
61
71
|
if (entry.readMeta?.meter) {
|
|
62
72
|
meterRoute(entry.readMeta.meter)
|
|
63
73
|
if (entry.adapter !== 'meter' || entry.mode !== 'read' || entry.readMeta.access !== 'ro' ||
|
|
@@ -124,6 +134,7 @@ function validatedVisualBindings ({ format, entries, devices, targetsByDevice })
|
|
|
124
134
|
}
|
|
125
135
|
const target = write || read
|
|
126
136
|
const type = controlTypeForAdapter(raw.adapter, target)
|
|
137
|
+
const reverse = raw.adapter === 'switch' ? stateReverse(raw, write, read) : undefined
|
|
127
138
|
if (read?.meter && (!Number.isFinite(read.min) || !Number.isFinite(read.max) || read.min >= read.max)) {
|
|
128
139
|
throw Error(`Meter needs a finite device range: ${raw.id}`)
|
|
129
140
|
}
|
|
@@ -131,7 +142,7 @@ function validatedVisualBindings ({ format, entries, devices, targetsByDevice })
|
|
|
131
142
|
const identity = JSON.stringify([raw.device, read?.path, write?.path])
|
|
132
143
|
const modelKey = candidate.repeat ? `${candidate.model}#${raw.instanceKey}` : candidate.model
|
|
133
144
|
const previous = models.get(modelKey)
|
|
134
|
-
const realtime =
|
|
145
|
+
const realtime = raw.adapter === 'meter' || raw.adapter === 'xy-series'
|
|
135
146
|
if (previous && (realtime || previous.realtime) && previous.identity !== identity) {
|
|
136
147
|
throw Error(`Realtime model "${candidate.model}" is already used by another binding; use a separate Vue model for each independent telemetry target`)
|
|
137
148
|
}
|
|
@@ -140,6 +151,7 @@ function validatedVisualBindings ({ format, entries, devices, targetsByDevice })
|
|
|
140
151
|
result[raw.id] = {
|
|
141
152
|
locator: raw.locator, adapter: raw.adapter, type, label: raw.label.trim(), mode: raw.mode,
|
|
142
153
|
...(candidate.button ? { buttonBehavior: behavior } : {}),
|
|
154
|
+
...(raw.adapter === 'switch' ? { stateReverse: reverse } : {}),
|
|
143
155
|
...(candidate.model ? { model: candidate.model } : {}),
|
|
144
156
|
...(candidate.repeat ? { repeat: candidate.repeat, instanceKey: raw.instanceKey } : {}),
|
|
145
157
|
...(candidate.component ? { component: candidate.component } : {}),
|
|
@@ -147,8 +159,10 @@ function validatedVisualBindings ({ format, entries, devices, targetsByDevice })
|
|
|
147
159
|
? { uiRange: { min: candidate.min, max: candidate.max, step: candidate.step } } : {}),
|
|
148
160
|
device: raw.device,
|
|
149
161
|
writePath: write?.path || '', readPath: read?.path || '',
|
|
150
|
-
...(write ? { writeMeta: targetMeta(write, raw.onebased
|
|
151
|
-
|
|
162
|
+
...(write ? { writeMeta: targetMeta(write, raw.onebased,
|
|
163
|
+
raw.adapter === 'switch' ? switchMapping(write, undefined, reverse) : undefined) } : {}),
|
|
164
|
+
...(read ? { readMeta: targetMeta(read, raw.onebased,
|
|
165
|
+
raw.adapter === 'switch' ? switchMapping(read, undefined, reverse) : undefined) } : {})
|
|
152
166
|
}
|
|
153
167
|
}
|
|
154
168
|
return result
|
package/xy-series-frame.js
CHANGED
|
@@ -85,8 +85,14 @@ function createRoomXYSeriesReceiver (layout) {
|
|
|
85
85
|
(valid && points.some(point => !Array.isArray(point) || point.length !== 2 ||
|
|
86
86
|
!finite(point[0]) || !finite(point[1])))) return false
|
|
87
87
|
}
|
|
88
|
+
if (frame.sources !== undefined && (!Array.isArray(frame.sources) || frame.sources.length > 256 ||
|
|
89
|
+
frame.sources.some(source => typeof source?.device !== 'string' || source.device.length > 128 ||
|
|
90
|
+
typeof source.streamId !== 'string' || source.streamId.length > 128 ||
|
|
91
|
+
typeof source.epoch !== 'string' || source.epoch.length > 128 ||
|
|
92
|
+
!Number.isSafeInteger(source.seq) || source.seq < 0))) return false
|
|
88
93
|
latest = { values: frame.values.map(points => points.map(point => [point[0], point[1]])),
|
|
89
|
-
valid: frame.valid.slice(), seq: frame.seq
|
|
94
|
+
valid: frame.valid.slice(), seq: frame.seq,
|
|
95
|
+
...(frame.sources ? { sources: frame.sources.map(({ device, streamId, epoch, seq }) => ({ device, streamId, epoch, seq })) } : {}) }
|
|
90
96
|
pending = true
|
|
91
97
|
return true
|
|
92
98
|
},
|