node-red-contrib-room-tools 1.8.0 → 1.8.2
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 +6 -1
- package/README.md +5 -2
- package/control-panel.html +2 -2
- package/control-policy.js +353 -26
- package/package.json +5 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ControlPanel AI 开发契约
|
|
2
2
|
|
|
3
|
-
适用节点:`ui-control-panel` 1.8.
|
|
3
|
+
适用节点:`ui-control-panel` 1.8.2。
|
|
4
4
|
|
|
5
5
|
把本文件作为 AI 开发或修改 ControlPanel Template 前的强制上下文。目标是让
|
|
6
6
|
不同页面使用同一套控制语义,避免滑杆积压、重复队列、反馈回环和跨房间误发。
|
|
@@ -151,6 +151,11 @@ Vuetify 滑杆可把相同逻辑分别放在 `@update:model-value` 和 `@end`。
|
|
|
151
151
|
连续控件直接发送目标值,不主动查询当前值。匹配目标的被动反馈会自动清除 pending;
|
|
152
152
|
未匹配的较旧反馈只更新 `deviceState`,`controlDisplayValue()` 仍返回最后目标。
|
|
153
153
|
|
|
154
|
+
离散控件的下一条同 topic 真实反馈会解除 pending,并直接以该反馈值显示实际状态,
|
|
155
|
+
即使 Automator 反馈携带的是 MUSE provider path、与 Template 的 UI `key` 不同。若同一房间内
|
|
156
|
+
有多个相同 topic 的控件,反馈必须提供与 Template 一致的 `controlKey`,避免误确认其他控件。
|
|
157
|
+
连续控件仍只有在反馈值匹配最后目标时才解除 pending。
|
|
158
|
+
|
|
154
159
|
离散按钮必须显示设备反馈值,不能先改本地状态。标准写法:
|
|
155
160
|
|
|
156
161
|
```html
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ regions follow the same URL structure.
|
|
|
15
15
|
|
|
16
16
|
## Control traffic policy
|
|
17
17
|
|
|
18
|
-
Version 1.8.
|
|
18
|
+
Version 1.8.2 uses the same control semantics at the panel, gateway and device
|
|
19
19
|
driver boundaries:
|
|
20
20
|
|
|
21
21
|
- `power`, `source`, `preset` and `mute` are discrete controls. The panel sends
|
|
@@ -58,7 +58,10 @@ the selected room. `deviceState` contains reported device state only. Accepted
|
|
|
58
58
|
commands update the gateway's private desired state but do not change
|
|
59
59
|
`deviceState`; matching Script-as-Device feedback is required before a Power,
|
|
60
60
|
Mute, Source or Preset control appears active. `isControlPending(key)` can be
|
|
61
|
-
used for a waiting indicator.
|
|
61
|
+
used for a waiting indicator. A discrete control is released by the next
|
|
62
|
+
authoritative feedback for its topic, even when Automator reports a provider
|
|
63
|
+
path instead of the Template's UI key. Continuous controls still wait for the
|
|
64
|
+
reported value to match their latest target.
|
|
62
65
|
|
|
63
66
|
Non-slider controls must use `:model-value`, `:value`, `:class` or
|
|
64
67
|
`:aria-pressed` from `deviceState`/`deviceValue()` and must not optimistically
|
package/control-panel.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const template = []
|
|
13
13
|
|
|
14
14
|
template.push('<!--')
|
|
15
|
-
template.push('AI / DEVELOPER CONTRACT - ui-control-panel 1.8.
|
|
15
|
+
template.push('AI / DEVELOPER CONTRACT - ui-control-panel 1.8.2')
|
|
16
16
|
template.push('AI START HERE: 先完整阅读 CONTROL_PANEL_AI_GUIDE.md、当前 ControlPanel Template 和 control-policy.js,再开发或修改页面。严格使用注入的 sendControl() 和设备反馈 API,禁止在 Template 中重写发送队列、限速、debounce、latest-wins、主动状态查询或反馈连接。所有非滑杆控件必须显示设备反馈确认的实际状态;滑杆必须直接发送并保持最后目标,不能被旧中间反馈拉回;设备反馈不能触发命令回发。完成后按照指南第 6 节逐项检查。')
|
|
17
17
|
template.push('1. Use the injected sendControl({ type, topic, key, value, final }) API for device commands.')
|
|
18
18
|
template.push('2. Never add debounce, throttle, latest-wins maps, Promise queues, or raw device TCP code here.')
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
template.push('5. key must be stable and unique per device control, for example rane/1/level/2.')
|
|
22
22
|
template.push('6. After room-gateway login call setControlSession(session); on logout call clearControlSession().')
|
|
23
23
|
template.push('7. Bind discrete control visuals to deviceState/deviceValue(), not to an optimistic local value.')
|
|
24
|
-
template.push('8. Use isControlPending(key) only for a waiting indicator;
|
|
24
|
+
template.push('8. Use isControlPending(key) only for a waiting indicator; discrete pending clears on authoritative topic feedback, while continuous pending waits for its target value.')
|
|
25
25
|
template.push('9. Sliders directly send and hold the latest target; use controlDisplayValue() so stale feedback cannot pull them back.')
|
|
26
26
|
template.push('10. Feedback may update UI state, but must never trigger a watcher that sends the value back.')
|
|
27
27
|
template.push('11. Do not redefine any injected ControlPanel API or its feedback state fields.')
|
package/control-policy.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const acorn = require('acorn')
|
|
2
|
+
const escodegen = require('escodegen')
|
|
3
|
+
|
|
1
4
|
const CONTROL_POLICY_MARKER = '/* ui-control-panel-control-api-v1 */'
|
|
2
5
|
const FEEDBACK_TOPIC_FIELDS = Object.freeze({
|
|
3
6
|
'display/power': 'display',
|
|
@@ -271,7 +274,6 @@ const __controlPanelPolicyMixin = {
|
|
|
271
274
|
},
|
|
272
275
|
__acknowledgeControlFeedback (feedback) {
|
|
273
276
|
for (const [key, request] of this.__controlPanelRequests) {
|
|
274
|
-
if (feedback.controlKey && feedback.controlKey !== key) continue
|
|
275
277
|
let matched = request.topic === 'room/reset'
|
|
276
278
|
let value
|
|
277
279
|
if (!matched && feedback.topic === request.topic) {
|
|
@@ -284,7 +286,15 @@ const __controlPanelPolicyMixin = {
|
|
|
284
286
|
value = feedback.state[field]
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
|
-
if (matched
|
|
289
|
+
if (!matched) continue
|
|
290
|
+
|
|
291
|
+
const sameTopicRequests = [...this.__controlPanelRequests.values()]
|
|
292
|
+
.filter(candidate => candidate.topic === request.topic).length
|
|
293
|
+
const correlated = !feedback.controlKey || feedback.controlKey === key || sameTopicRequests === 1
|
|
294
|
+
if (!correlated) continue
|
|
295
|
+
|
|
296
|
+
const continuous = ['level', 'dimmer', 'position'].includes(request.type)
|
|
297
|
+
if (!continuous || request.topic === 'room/reset' || Object.is(value, request.value)) {
|
|
288
298
|
this.__clearControlPending(key)
|
|
289
299
|
}
|
|
290
300
|
}
|
|
@@ -399,17 +409,344 @@ const __controlPanelPolicyMixin = {
|
|
|
399
409
|
`
|
|
400
410
|
}
|
|
401
411
|
|
|
412
|
+
// Dashboard 2.0 1.31 parses ui-template scripts itself. It only treats a script
|
|
413
|
+
// as component options when the script contains one `export default { ... }`
|
|
414
|
+
// statement. Keep every helper inside data, methods, or lifecycle hooks so the
|
|
415
|
+
// generated script remains inside that supported subset.
|
|
416
|
+
function buildDashboardPolicyOptionsSource () {
|
|
417
|
+
return `
|
|
418
|
+
export default {
|
|
419
|
+
data () {
|
|
420
|
+
return {
|
|
421
|
+
deviceState: {},
|
|
422
|
+
deviceFeedbackRevision: 0,
|
|
423
|
+
deviceFeedbackConnected: false,
|
|
424
|
+
controlPending: {},
|
|
425
|
+
controlTargets: {}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
methods: {
|
|
429
|
+
__controlPanelCreatePolicy: ${createControlPanelPolicy.toString()},
|
|
430
|
+
__controlPanelNormalizeFeedback: ${normalizeControlFeedback.toString()},
|
|
431
|
+
__controlPanelFeedbackFields () {
|
|
432
|
+
return ${JSON.stringify(FEEDBACK_TOPIC_FIELDS)}
|
|
433
|
+
},
|
|
434
|
+
__initializeControlPanelPolicy () {
|
|
435
|
+
this.__controlPanelSession = null
|
|
436
|
+
this.__controlPanelFeedbackSource = null
|
|
437
|
+
this.__controlPanelRequests = new Map()
|
|
438
|
+
this.__resetControlPolicy()
|
|
439
|
+
},
|
|
440
|
+
__disposeControlPanelPolicy () {
|
|
441
|
+
this.__closeControlFeedback()
|
|
442
|
+
this.__controlPanelPolicy?.dispose()
|
|
443
|
+
this.__controlPanelPolicy = null
|
|
444
|
+
this.__controlPanelRequests?.clear()
|
|
445
|
+
this.controlPending = {}
|
|
446
|
+
this.controlTargets = {}
|
|
447
|
+
this.__controlPanelSession = null
|
|
448
|
+
},
|
|
449
|
+
__resetControlPolicy () {
|
|
450
|
+
this.__controlPanelPolicy?.dispose()
|
|
451
|
+
this.__controlPanelPolicy = this.__controlPanelCreatePolicy({
|
|
452
|
+
intervalMs: 40,
|
|
453
|
+
dispatch: (command) => this.__dispatchControl(command),
|
|
454
|
+
onError: (error, command) => {
|
|
455
|
+
if (command?.controlKey) this.__clearControlPending(command.controlKey)
|
|
456
|
+
if (typeof this.onControlError === 'function') this.onControlError(error, command)
|
|
457
|
+
else console.error('ControlPanel command failed', error)
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
},
|
|
461
|
+
__setControlPending (key, pending) {
|
|
462
|
+
const next = { ...this.controlPending }
|
|
463
|
+
if (pending) next[key] = true
|
|
464
|
+
else delete next[key]
|
|
465
|
+
this.controlPending = next
|
|
466
|
+
},
|
|
467
|
+
__setControlTarget (key, value) {
|
|
468
|
+
this.controlTargets = { ...this.controlTargets, [key]: value }
|
|
469
|
+
},
|
|
470
|
+
__clearControlPending (key) {
|
|
471
|
+
this.__controlPanelRequests.delete(String(key))
|
|
472
|
+
this.__setControlPending(String(key), false)
|
|
473
|
+
const targets = { ...this.controlTargets }
|
|
474
|
+
delete targets[String(key)]
|
|
475
|
+
this.controlTargets = targets
|
|
476
|
+
},
|
|
477
|
+
__setFeedbackConnected (connected) {
|
|
478
|
+
const next = connected === true
|
|
479
|
+
if (this.deviceFeedbackConnected === next) return
|
|
480
|
+
this.deviceFeedbackConnected = next
|
|
481
|
+
if (typeof this.onControlFeedbackStatus === 'function') this.onControlFeedbackStatus(next)
|
|
482
|
+
},
|
|
483
|
+
__closeControlFeedback () {
|
|
484
|
+
this.__controlPanelFeedbackSource?.close()
|
|
485
|
+
this.__controlPanelFeedbackSource = null
|
|
486
|
+
this.__setFeedbackConnected(false)
|
|
487
|
+
},
|
|
488
|
+
__connectControlFeedback () {
|
|
489
|
+
this.__closeControlFeedback()
|
|
490
|
+
const session = this.__controlPanelSession
|
|
491
|
+
if (!session || typeof EventSource === 'undefined') return
|
|
492
|
+
const source = new EventSource('/room-panel-api/regions/' + encodeURIComponent(session.room) + '/events')
|
|
493
|
+
this.__controlPanelFeedbackSource = source
|
|
494
|
+
source.onopen = () => {
|
|
495
|
+
if (this.__controlPanelFeedbackSource === source) this.__setFeedbackConnected(true)
|
|
496
|
+
}
|
|
497
|
+
source.onmessage = (event) => {
|
|
498
|
+
if (this.__controlPanelFeedbackSource !== source) return
|
|
499
|
+
try {
|
|
500
|
+
this.applyControlFeedback(JSON.parse(event.data))
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (typeof this.onControlError === 'function') this.onControlError(error)
|
|
503
|
+
else console.error('ControlPanel feedback failed', error)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
source.onerror = () => {
|
|
507
|
+
if (this.__controlPanelFeedbackSource === source) this.__setFeedbackConnected(false)
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
__acknowledgeControlFeedback (feedback) {
|
|
511
|
+
const feedbackFields = this.__controlPanelFeedbackFields()
|
|
512
|
+
for (const [key, request] of this.__controlPanelRequests) {
|
|
513
|
+
let matched = request.topic === 'room/reset'
|
|
514
|
+
let value
|
|
515
|
+
if (!matched && feedback.topic === request.topic) {
|
|
516
|
+
matched = true
|
|
517
|
+
value = feedback.payload
|
|
518
|
+
} else if (!matched && feedback.topic === 'room/state') {
|
|
519
|
+
const field = feedbackFields[request.topic]
|
|
520
|
+
if (field && Object.prototype.hasOwnProperty.call(feedback.state, field)) {
|
|
521
|
+
matched = true
|
|
522
|
+
value = feedback.state[field]
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!matched) continue
|
|
526
|
+
|
|
527
|
+
const sameTopicRequests = [...this.__controlPanelRequests.values()]
|
|
528
|
+
.filter(candidate => candidate.topic === request.topic).length
|
|
529
|
+
const correlated = !feedback.controlKey || feedback.controlKey === key || sameTopicRequests === 1
|
|
530
|
+
if (!correlated) continue
|
|
531
|
+
|
|
532
|
+
const continuous = ['level', 'dimmer', 'position'].includes(request.type)
|
|
533
|
+
if (!continuous || request.topic === 'room/reset' || Object.is(value, request.value)) {
|
|
534
|
+
this.__clearControlPending(key)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
setControlSession (session) {
|
|
539
|
+
if (!session || typeof session !== 'object' || !session.room || !session.csrf) {
|
|
540
|
+
throw new Error('ControlPanel session needs room and csrf')
|
|
541
|
+
}
|
|
542
|
+
const next = { room: String(session.room), csrf: String(session.csrf) }
|
|
543
|
+
if (this.__controlPanelSession &&
|
|
544
|
+
(this.__controlPanelSession.room !== next.room || this.__controlPanelSession.csrf !== next.csrf)) {
|
|
545
|
+
this.__resetControlPolicy()
|
|
546
|
+
this.__controlPanelRequests.clear()
|
|
547
|
+
this.controlPending = {}
|
|
548
|
+
this.controlTargets = {}
|
|
549
|
+
this.deviceState = {}
|
|
550
|
+
this.deviceFeedbackRevision = 0
|
|
551
|
+
}
|
|
552
|
+
this.__controlPanelSession = next
|
|
553
|
+
this.__connectControlFeedback()
|
|
554
|
+
},
|
|
555
|
+
clearControlSession () {
|
|
556
|
+
this.__closeControlFeedback()
|
|
557
|
+
this.__controlPanelSession = null
|
|
558
|
+
this.__resetControlPolicy()
|
|
559
|
+
this.__controlPanelRequests.clear()
|
|
560
|
+
this.controlPending = {}
|
|
561
|
+
this.controlTargets = {}
|
|
562
|
+
this.deviceState = {}
|
|
563
|
+
this.deviceFeedbackRevision = 0
|
|
564
|
+
},
|
|
565
|
+
sendControl (command) {
|
|
566
|
+
const key = String(command?.key ?? command?.controlKey ?? '')
|
|
567
|
+
const type = String(command?.type ?? command?.controlType ?? '').toLowerCase()
|
|
568
|
+
const value = Object.prototype.hasOwnProperty.call(command || {}, 'value') ? command.value : command?.payload
|
|
569
|
+
if (key) {
|
|
570
|
+
this.__controlPanelRequests.set(key, {
|
|
571
|
+
topic: String(command.topic ?? ''),
|
|
572
|
+
type,
|
|
573
|
+
value
|
|
574
|
+
})
|
|
575
|
+
this.__setControlPending(key, true)
|
|
576
|
+
if (['level', 'dimmer', 'position'].includes(type)) this.__setControlTarget(key, value)
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
return this.__controlPanelPolicy.send(command)
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (key) this.__clearControlPending(key)
|
|
582
|
+
throw error
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
flushControl (key) {
|
|
586
|
+
return this.__controlPanelPolicy.flush(key)
|
|
587
|
+
},
|
|
588
|
+
applyControlFeedback (message) {
|
|
589
|
+
const room = this.__controlPanelSession?.room || this.authorizedRoom
|
|
590
|
+
const feedback = this.__controlPanelNormalizeFeedback(
|
|
591
|
+
message,
|
|
592
|
+
room,
|
|
593
|
+
this.__controlPanelFeedbackFields()
|
|
594
|
+
)
|
|
595
|
+
this.deviceState = { ...this.deviceState, ...feedback.state }
|
|
596
|
+
if (feedback.revision !== null) this.deviceFeedbackRevision = feedback.revision
|
|
597
|
+
this.__acknowledgeControlFeedback(feedback)
|
|
598
|
+
if (typeof this.onDeviceFeedback === 'function') this.onDeviceFeedback(feedback, this.deviceState)
|
|
599
|
+
return feedback
|
|
600
|
+
},
|
|
601
|
+
hasDeviceState (field) {
|
|
602
|
+
return Object.prototype.hasOwnProperty.call(this.deviceState, String(field))
|
|
603
|
+
},
|
|
604
|
+
deviceValue (field, fallback = null) {
|
|
605
|
+
return this.hasDeviceState(field) ? this.deviceState[String(field)] : fallback
|
|
606
|
+
},
|
|
607
|
+
isControlPending (key) {
|
|
608
|
+
return this.controlPending[String(key)] === true
|
|
609
|
+
},
|
|
610
|
+
controlDisplayValue (key, field, fallback = null) {
|
|
611
|
+
const controlKey = String(key)
|
|
612
|
+
if (this.isControlPending(controlKey) &&
|
|
613
|
+
Object.prototype.hasOwnProperty.call(this.controlTargets, controlKey)) {
|
|
614
|
+
return this.controlTargets[controlKey]
|
|
615
|
+
}
|
|
616
|
+
return this.deviceValue(field, fallback)
|
|
617
|
+
},
|
|
618
|
+
async __dispatchControl (command) {
|
|
619
|
+
if (typeof this.controlTransport === 'function') return this.controlTransport(command)
|
|
620
|
+
|
|
621
|
+
const session = this.__controlPanelSession
|
|
622
|
+
const room = session?.room || this.authorizedRoom
|
|
623
|
+
if (typeof this.postControl === 'function') {
|
|
624
|
+
return this.postControl(room ? { ...command, room } : command, this.sessionGeneration)
|
|
625
|
+
}
|
|
626
|
+
if (!session) throw new Error('Call setControlSession() after login, or define controlTransport(command)')
|
|
627
|
+
|
|
628
|
+
const response = await fetch('/room-panel-api/regions/' + encodeURIComponent(session.room) + '/command', {
|
|
629
|
+
method: 'POST',
|
|
630
|
+
credentials: 'same-origin',
|
|
631
|
+
headers: { 'Content-Type': 'application/json', 'X-Room-CSRF': session.csrf },
|
|
632
|
+
body: JSON.stringify({ room: session.room, ...command }),
|
|
633
|
+
signal: AbortSignal.timeout(8000)
|
|
634
|
+
})
|
|
635
|
+
if (!response.ok) throw new Error('Control command failed (' + response.status + ')')
|
|
636
|
+
return response
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
mounted () {
|
|
640
|
+
this.__initializeControlPanelPolicy()
|
|
641
|
+
},
|
|
642
|
+
unmounted () {
|
|
643
|
+
this.__disposeControlPanelPolicy()
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
`
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function parseOptionsObject (source) {
|
|
650
|
+
const tree = acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module' })
|
|
651
|
+
if (tree.body.length !== 1 || tree.body[0].type !== 'ExportDefaultDeclaration' ||
|
|
652
|
+
tree.body[0].declaration.type !== 'ObjectExpression') return null
|
|
653
|
+
return tree.body[0].declaration
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function propertyName (property) {
|
|
657
|
+
if (!property || property.type !== 'Property') return null
|
|
658
|
+
if (!property.computed && property.key.type === 'Identifier') return property.key.name
|
|
659
|
+
if (property.key.type === 'Literal') return String(property.key.value)
|
|
660
|
+
return null
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function findProperty (object, name) {
|
|
664
|
+
return object.properties.find(property => propertyName(property) === name)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function parseProperty (source) {
|
|
668
|
+
const object = acorn.parse('({' + source + '})', { ecmaVersion: 'latest' }).body[0].expression
|
|
669
|
+
return object.properties[0]
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function functionSource (property) {
|
|
673
|
+
return property ? escodegen.generate(property.value) : null
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function mergeDashboardOptions (userSource) {
|
|
677
|
+
const user = parseOptionsObject(userSource)
|
|
678
|
+
if (!user) return null
|
|
679
|
+
|
|
680
|
+
const policy = parseOptionsObject(buildDashboardPolicyOptionsSource())
|
|
681
|
+
const policyData = findProperty(policy, 'data')
|
|
682
|
+
const policyMethods = findProperty(policy, 'methods')
|
|
683
|
+
const policyMounted = findProperty(policy, 'mounted')
|
|
684
|
+
const policyUnmounted = findProperty(policy, 'unmounted')
|
|
685
|
+
const userData = findProperty(user, 'data')
|
|
686
|
+
const userMethods = findProperty(user, 'methods')
|
|
687
|
+
const userMounted = findProperty(user, 'mounted')
|
|
688
|
+
const userUnmounted = findProperty(user, 'unmounted')
|
|
689
|
+
|
|
690
|
+
if (userMethods && userMethods.value.type !== 'ObjectExpression') return null
|
|
691
|
+
|
|
692
|
+
const data = userData
|
|
693
|
+
? parseProperty(`data () {
|
|
694
|
+
const userState = (${functionSource(userData)}).call(this) || {}
|
|
695
|
+
const policyState = (${functionSource(policyData)}).call(this) || {}
|
|
696
|
+
return { ...userState, ...policyState }
|
|
697
|
+
}`)
|
|
698
|
+
: policyData
|
|
699
|
+
|
|
700
|
+
const policyMethodNames = new Set(policyMethods.value.properties.map(propertyName))
|
|
701
|
+
const methods = parseProperty('methods: {}')
|
|
702
|
+
methods.value.properties = [
|
|
703
|
+
...(userMethods ? userMethods.value.properties.filter(property => !policyMethodNames.has(propertyName(property))) : []),
|
|
704
|
+
...policyMethods.value.properties
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
const mounted = userMounted
|
|
708
|
+
? parseProperty(`mounted () {
|
|
709
|
+
(${functionSource(policyMounted)}).call(this)
|
|
710
|
+
return (${functionSource(userMounted)}).call(this)
|
|
711
|
+
}`)
|
|
712
|
+
: policyMounted
|
|
713
|
+
|
|
714
|
+
const unmounted = userUnmounted
|
|
715
|
+
? parseProperty(`unmounted () {
|
|
716
|
+
try {
|
|
717
|
+
return (${functionSource(userUnmounted)}).call(this)
|
|
718
|
+
} finally {
|
|
719
|
+
(${functionSource(policyUnmounted)}).call(this)
|
|
720
|
+
}
|
|
721
|
+
}`)
|
|
722
|
+
: policyUnmounted
|
|
723
|
+
|
|
724
|
+
const replaced = new Set(['data', 'methods', 'mounted', 'unmounted'])
|
|
725
|
+
const merged = {
|
|
726
|
+
...user,
|
|
727
|
+
properties: [
|
|
728
|
+
...user.properties.filter(property => !replaced.has(propertyName(property))),
|
|
729
|
+
data,
|
|
730
|
+
methods,
|
|
731
|
+
mounted,
|
|
732
|
+
unmounted
|
|
733
|
+
]
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return CONTROL_POLICY_MARKER + '\nexport default ' + escodegen.generate(merged, {
|
|
737
|
+
format: { indent: { style: ' ' } }
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
|
|
402
741
|
function injectControlPolicy (format) {
|
|
403
742
|
if (typeof format !== 'string' || format.includes(CONTROL_POLICY_MARKER)) return format
|
|
404
743
|
|
|
405
744
|
const scriptMatch = /<script(?!\s+setup\b)(?:\s[^>]*)?>/i.exec(format)
|
|
406
745
|
if (!scriptMatch) {
|
|
407
746
|
const setupMatch = /<script\s+setup\b([^>]*)>/i.exec(format)
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return format + '\n' + scriptOpen + '\n' + buildControlPolicyMixinSource() +
|
|
412
|
-
'\nexport default { mixins: [__controlPanelPolicyMixin] }\n</script>\n'
|
|
747
|
+
if (setupMatch || !/<template(?:\s|>)/i.test(format)) return format
|
|
748
|
+
return format + '\n<script>\n' + CONTROL_POLICY_MARKER + '\n' +
|
|
749
|
+
buildDashboardPolicyOptionsSource().trim() + '\n</script>\n'
|
|
413
750
|
}
|
|
414
751
|
|
|
415
752
|
const scriptStart = scriptMatch.index + scriptMatch[0].length
|
|
@@ -417,33 +754,23 @@ function injectControlPolicy (format) {
|
|
|
417
754
|
if (scriptEnd === -1) return format
|
|
418
755
|
|
|
419
756
|
const source = format.slice(scriptStart, scriptEnd)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return format
|
|
757
|
+
let wrapped
|
|
758
|
+
try {
|
|
759
|
+
wrapped = mergeDashboardOptions(source)
|
|
760
|
+
} catch (_) {
|
|
761
|
+
return format
|
|
425
762
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const exportEnd = exportMatch.index + exportMatch[0].length
|
|
429
|
-
const userOptions = source.slice(0, exportStart) +
|
|
430
|
-
'const __controlPanelUserOptions =' +
|
|
431
|
-
source.slice(exportEnd)
|
|
432
|
-
const wrapped = userOptions + '\n' + buildControlPolicyMixinSource() + `
|
|
433
|
-
export default {
|
|
434
|
-
...__controlPanelUserOptions,
|
|
435
|
-
mixins: [...(__controlPanelUserOptions.mixins || []), __controlPanelPolicyMixin]
|
|
436
|
-
}
|
|
437
|
-
`
|
|
438
|
-
|
|
439
|
-
return format.slice(0, scriptStart) + wrapped + format.slice(scriptEnd)
|
|
763
|
+
if (!wrapped) return format
|
|
764
|
+
return format.slice(0, scriptStart) + '\n' + wrapped + '\n' + format.slice(scriptEnd)
|
|
440
765
|
}
|
|
441
766
|
|
|
442
767
|
module.exports = {
|
|
443
768
|
CONTROL_POLICY_MARKER,
|
|
444
769
|
FEEDBACK_TOPIC_FIELDS,
|
|
445
770
|
buildControlPolicyMixinSource,
|
|
771
|
+
buildDashboardPolicyOptionsSource,
|
|
446
772
|
createControlPanelPolicy,
|
|
447
773
|
injectControlPolicy,
|
|
774
|
+
mergeDashboardOptions,
|
|
448
775
|
normalizeControlFeedback
|
|
449
776
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-room-tools",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "Room panel message generator and authenticated region gateway for Node-RED and MUSE Automator",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"keywords": ["node-red"],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"acorn": "^8.15.0",
|
|
9
|
+
"escodegen": "^2.1.0"
|
|
10
|
+
},
|
|
7
11
|
"files": ["gateway.cjs", "room-gateway.js", "room-gateway.html", "room-message.js", "room-message.html", "control-panel.js", "control-panel.html", "control-policy.js", "CONTROL_PANEL_AI_GUIDE.md", "README.md"],
|
|
8
12
|
"node-red": {
|
|
9
13
|
"nodes": {
|