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.
@@ -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.type !== 'boolean' &&
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) && !['integer', 'float'].includes(target.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' && (!target.meter || !['integer', 'float'].includes(target.type) || target.access !== 'ro')) {
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 lanes follow ordinary feedback lanes without overlapping their labels.
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, y: Math.round((390 + (readTargets.size + meterStreams.size + 1) * 85) / 10) * 10,
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,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "node-red-contrib-room-tools",
3
- "version": "1.9.3",
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.3",
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",
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",