node-red-contrib-room-tools 1.8.4 → 1.8.8

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-policy.js CHANGED
@@ -1,50 +1,11 @@
1
1
  const acorn = require('acorn')
2
2
  const escodegen = require('escodegen')
3
3
  const { initializeVisualBinding, disposeVisualBinding } = require('./panel-binding-editor')
4
+ const { fetchWithTimeout } = require('./panel-http')
5
+ const { createTargetMatcher } = require('./control-type-model')
4
6
 
5
7
  const CONTROL_POLICY_MARKER = '/* ui-control-panel-control-api-v1 */'
6
- const FEEDBACK_TOPIC_FIELDS = Object.freeze({
7
- 'display/power': 'display',
8
- 'display/source': 'source',
9
- 'audio/volume': 'volume',
10
- 'audio/mute': 'muted',
11
- 'lighting/brightness': 'light',
12
- 'room/curtain': 'curtains',
13
- 'climate/temperature': 'temperature',
14
- 'room/scene': 'scene',
15
- 'room/meeting': 'meeting'
16
- })
17
8
 
18
- function normalizeControlFeedback (message, expectedRoom, topicFields) {
19
- if (!message || typeof message !== 'object' || Array.isArray(message)) {
20
- throw new Error('Control feedback must be an object')
21
- }
22
- if (message.kind !== 'state') throw new Error('Control feedback must have kind=state')
23
- if (expectedRoom && message.room !== expectedRoom) throw new Error('Control feedback room mismatch')
24
- if (typeof message.topic !== 'string' || !message.topic) throw new Error('Control feedback needs a topic')
25
-
26
- let state = message.state && typeof message.state === 'object' && !Array.isArray(message.state)
27
- ? { ...message.state }
28
- : {}
29
- if (message.topic === 'room/state') {
30
- if (!message.payload || typeof message.payload !== 'object' || Array.isArray(message.payload)) {
31
- throw new Error('room/state feedback needs an object payload')
32
- }
33
- state = { ...state, ...message.payload }
34
- } else if (topicFields[message.topic]) {
35
- state[topicFields[message.topic]] = message.payload
36
- }
37
-
38
- return {
39
- room: message.room,
40
- kind: 'state',
41
- topic: message.topic,
42
- payload: message.payload,
43
- state,
44
- revision: Number.isInteger(message.revision) && message.revision >= 0 ? message.revision : null,
45
- controlKey: typeof message.controlKey === 'string' ? message.controlKey : null
46
- }
47
- }
48
9
 
49
10
  function createControlPanelPolicy ({
50
11
  dispatch,
@@ -189,226 +150,6 @@ function createControlPanelPolicy ({
189
150
  }
190
151
  }
191
152
 
192
- function buildControlPolicyMixinSource () {
193
- return `
194
- ${CONTROL_POLICY_MARKER}
195
- const __controlPanelCreatePolicy = ${createControlPanelPolicy.toString()}
196
- const __controlPanelFeedbackFields = ${JSON.stringify(FEEDBACK_TOPIC_FIELDS)}
197
- const __controlPanelNormalizeFeedback = ${normalizeControlFeedback.toString()}
198
- const __controlPanelPolicyMixin = {
199
- data () {
200
- return {
201
- deviceState: {},
202
- deviceFeedbackRevision: 0,
203
- deviceFeedbackConnected: false,
204
- controlPending: {},
205
- controlTargets: {}
206
- }
207
- },
208
- created () {
209
- this.__controlPanelSession = null
210
- this.__controlPanelFeedbackSource = null
211
- this.__controlPanelRequests = new Map()
212
- this.__resetControlPolicy()
213
- },
214
- methods: {
215
- __resetControlPolicy () {
216
- this.__controlPanelPolicy?.dispose()
217
- this.__controlPanelPolicy = __controlPanelCreatePolicy({
218
- intervalMs: 40,
219
- dispatch: (command) => this.__dispatchControl(command),
220
- onError: (error, command) => {
221
- if (command?.controlKey) this.__clearControlPending(command.controlKey)
222
- if (typeof this.onControlError === 'function') this.onControlError(error, command)
223
- else console.error('ControlPanel command failed', error)
224
- }
225
- })
226
- },
227
- __setControlPending (key, pending) {
228
- const next = { ...this.controlPending }
229
- if (pending) next[key] = true
230
- else delete next[key]
231
- this.controlPending = next
232
- },
233
- __setControlTarget (key, value) {
234
- this.controlTargets = { ...this.controlTargets, [key]: value }
235
- },
236
- __clearControlPending (key) {
237
- this.__controlPanelRequests.delete(String(key))
238
- this.__setControlPending(String(key), false)
239
- const targets = { ...this.controlTargets }
240
- delete targets[String(key)]
241
- this.controlTargets = targets
242
- },
243
- __setFeedbackConnected (connected) {
244
- const next = connected === true
245
- if (this.deviceFeedbackConnected === next) return
246
- this.deviceFeedbackConnected = next
247
- if (typeof this.onControlFeedbackStatus === 'function') this.onControlFeedbackStatus(next)
248
- },
249
- __closeControlFeedback () {
250
- this.__controlPanelFeedbackSource?.close()
251
- this.__controlPanelFeedbackSource = null
252
- this.__setFeedbackConnected(false)
253
- },
254
- __connectControlFeedback () {
255
- this.__closeControlFeedback()
256
- const session = this.__controlPanelSession
257
- if (!session || typeof EventSource === 'undefined') return
258
- const source = new EventSource('/room-panel-api/regions/' + encodeURIComponent(session.room) + '/events')
259
- this.__controlPanelFeedbackSource = source
260
- source.onopen = () => {
261
- if (this.__controlPanelFeedbackSource === source) this.__setFeedbackConnected(true)
262
- }
263
- source.onmessage = (event) => {
264
- if (this.__controlPanelFeedbackSource !== source) return
265
- try {
266
- this.applyControlFeedback(JSON.parse(event.data))
267
- } catch (error) {
268
- if (typeof this.onControlError === 'function') this.onControlError(error)
269
- else console.error('ControlPanel feedback failed', error)
270
- }
271
- }
272
- source.onerror = () => {
273
- if (this.__controlPanelFeedbackSource === source) this.__setFeedbackConnected(false)
274
- }
275
- },
276
- __acknowledgeControlFeedback (feedback) {
277
- for (const [key, request] of this.__controlPanelRequests) {
278
- let matched = request.topic === 'room/reset'
279
- let value
280
- if (!matched && feedback.topic === request.topic) {
281
- matched = true
282
- value = feedback.payload
283
- } else if (!matched && feedback.topic === 'room/state') {
284
- const field = __controlPanelFeedbackFields[request.topic]
285
- if (field && Object.prototype.hasOwnProperty.call(feedback.state, field)) {
286
- matched = true
287
- value = feedback.state[field]
288
- }
289
- }
290
- if (!matched) continue
291
-
292
- const sameTopicRequests = [...this.__controlPanelRequests.values()]
293
- .filter(candidate => candidate.topic === request.topic).length
294
- const correlated = !feedback.controlKey || feedback.controlKey === key || sameTopicRequests === 1
295
- if (!correlated) continue
296
-
297
- const continuous = ['level', 'dimmer', 'position'].includes(request.type)
298
- if (!continuous || request.topic === 'room/reset' || Object.is(value, request.value)) {
299
- this.__clearControlPending(key)
300
- }
301
- }
302
- },
303
- setControlSession (session) {
304
- if (!session || typeof session !== 'object' || !session.room || !session.csrf) {
305
- throw new Error('ControlPanel session needs room and csrf')
306
- }
307
- const next = { room: String(session.room), csrf: String(session.csrf) }
308
- if (this.__controlPanelSession &&
309
- (this.__controlPanelSession.room !== next.room || this.__controlPanelSession.csrf !== next.csrf)) {
310
- this.__resetControlPolicy()
311
- this.__controlPanelRequests.clear()
312
- this.controlPending = {}
313
- this.controlTargets = {}
314
- this.deviceState = {}
315
- this.deviceFeedbackRevision = 0
316
- }
317
- this.__controlPanelSession = next
318
- this.__connectControlFeedback()
319
- },
320
- clearControlSession () {
321
- this.__closeControlFeedback()
322
- this.__controlPanelSession = null
323
- this.__resetControlPolicy()
324
- this.__controlPanelRequests.clear()
325
- this.controlPending = {}
326
- this.controlTargets = {}
327
- this.deviceState = {}
328
- this.deviceFeedbackRevision = 0
329
- },
330
- sendControl (command) {
331
- const key = String(command?.key ?? command?.controlKey ?? '')
332
- const type = String(command?.type ?? command?.controlType ?? '').toLowerCase()
333
- const value = Object.prototype.hasOwnProperty.call(command || {}, 'value') ? command.value : command?.payload
334
- if (key) {
335
- this.__controlPanelRequests.set(key, {
336
- topic: String(command.topic ?? ''),
337
- type,
338
- value
339
- })
340
- this.__setControlPending(key, true)
341
- if (['level', 'dimmer', 'position'].includes(type)) this.__setControlTarget(key, value)
342
- }
343
- try {
344
- return this.__controlPanelPolicy.send(command)
345
- } catch (error) {
346
- if (key) this.__clearControlPending(key)
347
- throw error
348
- }
349
- },
350
- flushControl (key) {
351
- return this.__controlPanelPolicy.flush(key)
352
- },
353
- applyControlFeedback (message) {
354
- const room = this.__controlPanelSession?.room || this.authorizedRoom
355
- const feedback = __controlPanelNormalizeFeedback(message, room, __controlPanelFeedbackFields)
356
- this.deviceState = { ...this.deviceState, ...feedback.state }
357
- if (feedback.revision !== null) this.deviceFeedbackRevision = feedback.revision
358
- this.__acknowledgeControlFeedback(feedback)
359
- if (typeof this.onDeviceFeedback === 'function') this.onDeviceFeedback(feedback, this.deviceState)
360
- return feedback
361
- },
362
- hasDeviceState (field) {
363
- return Object.prototype.hasOwnProperty.call(this.deviceState, String(field))
364
- },
365
- deviceValue (field, fallback = null) {
366
- return this.hasDeviceState(field) ? this.deviceState[String(field)] : fallback
367
- },
368
- isControlPending (key) {
369
- return this.controlPending[String(key)] === true
370
- },
371
- controlDisplayValue (key, field, fallback = null) {
372
- const controlKey = String(key)
373
- if (this.isControlPending(controlKey) &&
374
- Object.prototype.hasOwnProperty.call(this.controlTargets, controlKey)) {
375
- return this.controlTargets[controlKey]
376
- }
377
- return this.deviceValue(field, fallback)
378
- },
379
- async __dispatchControl (command) {
380
- if (typeof this.controlTransport === 'function') return this.controlTransport(command)
381
-
382
- const session = this.__controlPanelSession
383
- const room = session?.room || this.authorizedRoom
384
- if (typeof this.postControl === 'function') {
385
- return this.postControl(room ? { ...command, room } : command, this.sessionGeneration)
386
- }
387
- if (!session) throw new Error('Call setControlSession() after login, or define controlTransport(command)')
388
-
389
- const response = await fetch('/room-panel-api/regions/' + encodeURIComponent(session.room) + '/command', {
390
- method: 'POST',
391
- credentials: 'same-origin',
392
- headers: { 'Content-Type': 'application/json', 'X-Room-CSRF': session.csrf },
393
- body: JSON.stringify({ room: session.room, ...command }),
394
- signal: AbortSignal.timeout(8000)
395
- })
396
- if (!response.ok) throw new Error('Control command failed (' + response.status + ')')
397
- return response
398
- }
399
- },
400
- unmounted () {
401
- this.__closeControlFeedback()
402
- this.__controlPanelPolicy?.dispose()
403
- this.__controlPanelPolicy = null
404
- this.__controlPanelRequests?.clear()
405
- this.controlPending = {}
406
- this.controlTargets = {}
407
- this.__controlPanelSession = null
408
- }
409
- }
410
- `
411
- }
412
153
 
413
154
  // Dashboard 2.0 1.31 parses ui-template scripts itself. It only treats a script
414
155
  // as component options when the script contains one `export default { ... }`
@@ -417,7 +158,8 @@ const __controlPanelPolicyMixin = {
417
158
  function buildDashboardPolicyOptionsSource (controls = [], boundPanel = null) {
418
159
  const boundControls = controls.map(({ id, type, mode }) => ({ id, type, mode }))
419
160
  const visualMethods = boundPanel?.visual
420
- ? `__initializeVisualBinding: ${initializeVisualBinding.toString()},
161
+ ? `__createTargetMatcher: ${createTargetMatcher.toString()},
162
+ __initializeVisualBinding: ${initializeVisualBinding.toString()},
421
163
  __disposeVisualBinding: ${disposeVisualBinding.toString()},`
422
164
  : ''
423
165
  const visualMounted = boundPanel?.visual
@@ -435,15 +177,11 @@ export default {
435
177
  boundFeedbackRevision: 0,
436
178
  boundFeedbackConnected: false,
437
179
  boundAuthenticated: false,
438
- deviceState: {},
439
- deviceFeedbackRevision: 0,
440
- deviceFeedbackConnected: false,
441
- controlPending: {},
442
- controlTargets: {}
443
180
  }
444
181
  },
445
182
  methods: {
446
183
  ${visualMethods}
184
+ __panelFetch: ${fetchWithTimeout.toString()},
447
185
  __boundPanelConfig () {
448
186
  return ${JSON.stringify(boundPanel)}
449
187
  },
@@ -555,14 +293,7 @@ export default {
555
293
  }
556
294
  },
557
295
  __controlPanelCreatePolicy: ${createControlPanelPolicy.toString()},
558
- __controlPanelNormalizeFeedback: ${normalizeControlFeedback.toString()},
559
- __controlPanelFeedbackFields () {
560
- return ${JSON.stringify(FEEDBACK_TOPIC_FIELDS)}
561
- },
562
296
  __initializeControlPanelPolicy () {
563
- this.__controlPanelSession = null
564
- this.__controlPanelFeedbackSource = null
565
- this.__controlPanelRequests = new Map()
566
297
  this.__boundPanelSession = null
567
298
  this.__boundPanelSource = null
568
299
  this.__boundAuthGeneration = 0
@@ -576,14 +307,9 @@ export default {
576
307
  this.__boundUnwatch?.()
577
308
  this.__boundUnwatch = null
578
309
  this.__clearAllBoundSettlements()
579
- this.__closeControlFeedback()
580
310
  this.__closeBoundFeedback()
581
311
  this.__controlPanelPolicy?.dispose()
582
312
  this.__controlPanelPolicy = null
583
- this.__controlPanelRequests?.clear()
584
- this.controlPending = {}
585
- this.controlTargets = {}
586
- this.__controlPanelSession = null
587
313
  this.__boundPanelSession = null
588
314
  },
589
315
  __resetControlPolicy () {
@@ -595,116 +321,11 @@ export default {
595
321
  if (command?.topic === '__bound' && command.controlKey) {
596
322
  this.__clearBoundPending(command.controlKey)
597
323
  }
598
- if (command?.controlKey) this.__clearControlPending(command.controlKey)
599
324
  if (typeof this.onControlError === 'function') this.onControlError(error, command)
600
325
  else console.error('ControlPanel command failed', error)
601
326
  }
602
327
  })
603
328
  },
604
- __setControlPending (key, pending) {
605
- const next = { ...this.controlPending }
606
- if (pending) next[key] = true
607
- else delete next[key]
608
- this.controlPending = next
609
- },
610
- __setControlTarget (key, value) {
611
- this.controlTargets = { ...this.controlTargets, [key]: value }
612
- },
613
- __clearControlPending (key) {
614
- this.__controlPanelRequests.delete(String(key))
615
- this.__setControlPending(String(key), false)
616
- const targets = { ...this.controlTargets }
617
- delete targets[String(key)]
618
- this.controlTargets = targets
619
- },
620
- __setFeedbackConnected (connected) {
621
- const next = connected === true
622
- if (this.deviceFeedbackConnected === next) return
623
- this.deviceFeedbackConnected = next
624
- if (typeof this.onControlFeedbackStatus === 'function') this.onControlFeedbackStatus(next)
625
- },
626
- __closeControlFeedback () {
627
- this.__controlPanelFeedbackSource?.close()
628
- this.__controlPanelFeedbackSource = null
629
- this.__setFeedbackConnected(false)
630
- },
631
- __connectControlFeedback () {
632
- this.__closeControlFeedback()
633
- const session = this.__controlPanelSession
634
- if (!session || typeof EventSource === 'undefined') return
635
- const source = new EventSource('/room-panel-api/regions/' + encodeURIComponent(session.room) + '/events')
636
- this.__controlPanelFeedbackSource = source
637
- source.onopen = () => {
638
- if (this.__controlPanelFeedbackSource === source) this.__setFeedbackConnected(true)
639
- }
640
- source.onmessage = (event) => {
641
- if (this.__controlPanelFeedbackSource !== source) return
642
- try {
643
- this.applyControlFeedback(JSON.parse(event.data))
644
- } catch (error) {
645
- if (typeof this.onControlError === 'function') this.onControlError(error)
646
- else console.error('ControlPanel feedback failed', error)
647
- }
648
- }
649
- source.onerror = () => {
650
- if (this.__controlPanelFeedbackSource === source) this.__setFeedbackConnected(false)
651
- }
652
- },
653
- __acknowledgeControlFeedback (feedback) {
654
- const feedbackFields = this.__controlPanelFeedbackFields()
655
- for (const [key, request] of this.__controlPanelRequests) {
656
- let matched = request.topic === 'room/reset'
657
- let value
658
- if (!matched && feedback.topic === request.topic) {
659
- matched = true
660
- value = feedback.payload
661
- } else if (!matched && feedback.topic === 'room/state') {
662
- const field = feedbackFields[request.topic]
663
- if (field && Object.prototype.hasOwnProperty.call(feedback.state, field)) {
664
- matched = true
665
- value = feedback.state[field]
666
- }
667
- }
668
- if (!matched) continue
669
-
670
- const sameTopicRequests = [...this.__controlPanelRequests.values()]
671
- .filter(candidate => candidate.topic === request.topic).length
672
- const correlated = !feedback.controlKey || feedback.controlKey === key || sameTopicRequests === 1
673
- if (!correlated) continue
674
-
675
- const continuous = ['level', 'dimmer', 'position'].includes(request.type)
676
- if (!continuous || request.topic === 'room/reset' || Object.is(value, request.value)) {
677
- this.__clearControlPending(key)
678
- }
679
- }
680
- },
681
- setControlSession (session) {
682
- if (!session || typeof session !== 'object' || !session.room || !session.csrf) {
683
- throw new Error('ControlPanel session needs room and csrf')
684
- }
685
- const next = { room: String(session.room), csrf: String(session.csrf) }
686
- if (this.__controlPanelSession &&
687
- (this.__controlPanelSession.room !== next.room || this.__controlPanelSession.csrf !== next.csrf)) {
688
- this.__resetControlPolicy()
689
- this.__controlPanelRequests.clear()
690
- this.controlPending = {}
691
- this.controlTargets = {}
692
- this.deviceState = {}
693
- this.deviceFeedbackRevision = 0
694
- }
695
- this.__controlPanelSession = next
696
- this.__connectControlFeedback()
697
- },
698
- clearControlSession () {
699
- this.__closeControlFeedback()
700
- this.__controlPanelSession = null
701
- this.__resetControlPolicy()
702
- this.__controlPanelRequests.clear()
703
- this.controlPending = {}
704
- this.controlTargets = {}
705
- this.deviceState = {}
706
- this.deviceFeedbackRevision = 0
707
- },
708
329
  __closeBoundFeedback () {
709
330
  this.__boundPanelSource?.close()
710
331
  this.__boundPanelSource = null
@@ -760,7 +381,7 @@ export default {
760
381
  if (!config) throw new Error('This panel has no bound login endpoint')
761
382
  this.clearBoundPanelSession()
762
383
  const generation = this.__boundAuthGeneration
763
- const response = await fetch(config.endpoint + '/login', {
384
+ const response = await this.__panelFetch(config.endpoint + '/login', {
764
385
  method: 'POST', credentials: 'same-origin',
765
386
  headers: { 'Content-Type': 'application/json' },
766
387
  body: JSON.stringify({ username, password })
@@ -775,7 +396,7 @@ export default {
775
396
  const config = this.__boundPanelConfig()
776
397
  if (!config) return null
777
398
  const generation = this.__boundAuthGeneration
778
- const response = await fetch(config.endpoint + '/session', { credentials: 'same-origin' })
399
+ const response = await this.__panelFetch(config.endpoint + '/session', { credentials: 'same-origin' })
779
400
  if (generation !== this.__boundAuthGeneration) return this.__boundPanelSession
780
401
  if (!response.ok) {
781
402
  this.clearBoundPanelSession()
@@ -791,106 +412,46 @@ export default {
791
412
  const session = this.__boundPanelSession
792
413
  this.clearBoundPanelSession()
793
414
  if (!config || !session) return
794
- const response = await fetch(config.endpoint + '/logout', {
415
+ const response = await this.__panelFetch(config.endpoint + '/logout', {
795
416
  method: 'POST', credentials: 'same-origin',
796
417
  headers: { 'Content-Type': 'application/json', 'X-Room-CSRF': session.csrf },
797
418
  body: '{}'
798
419
  })
799
420
  if (!response.ok) throw new Error('Panel logout failed (' + response.status + ')')
800
421
  },
801
- sendControl (command) {
802
- if (command && Object.prototype.hasOwnProperty.call(command, 'id')) {
803
- return this.sendBoundControl(command)
804
- }
805
- const key = String(command?.key ?? command?.controlKey ?? '')
806
- const type = String(command?.type ?? command?.controlType ?? '').toLowerCase()
807
- const value = Object.prototype.hasOwnProperty.call(command || {}, 'value') ? command.value : command?.payload
808
- if (key) {
809
- this.__controlPanelRequests.set(key, {
810
- topic: String(command.topic ?? ''),
811
- type,
812
- value
813
- })
814
- this.__setControlPending(key, true)
815
- if (['level', 'dimmer', 'position'].includes(type)) this.__setControlTarget(key, value)
816
- }
817
- try {
818
- return this.__controlPanelPolicy.send(command)
819
- } catch (error) {
820
- if (key) this.__clearControlPending(key)
821
- throw error
822
- }
823
- },
824
422
  flushControl (key) {
825
423
  return this.__controlPanelPolicy.flush(key)
826
424
  },
827
- applyControlFeedback (message) {
828
- const room = this.__controlPanelSession?.room || this.authorizedRoom
829
- const feedback = this.__controlPanelNormalizeFeedback(
830
- message,
831
- room,
832
- this.__controlPanelFeedbackFields()
833
- )
834
- this.deviceState = { ...this.deviceState, ...feedback.state }
835
- if (feedback.revision !== null) this.deviceFeedbackRevision = feedback.revision
836
- this.__acknowledgeControlFeedback(feedback)
837
- if (typeof this.onDeviceFeedback === 'function') this.onDeviceFeedback(feedback, this.deviceState)
838
- return feedback
839
- },
840
- hasDeviceState (field) {
841
- return Object.prototype.hasOwnProperty.call(this.deviceState, String(field))
842
- },
843
- deviceValue (field, fallback = null) {
844
- return this.hasDeviceState(field) ? this.deviceState[String(field)] : fallback
845
- },
846
- isControlPending (key) {
847
- return this.controlPending[String(key)] === true
848
- },
849
- controlDisplayValue (key, field, fallback = null) {
850
- const controlKey = String(key)
851
- if (this.isControlPending(controlKey) &&
852
- Object.prototype.hasOwnProperty.call(this.controlTargets, controlKey)) {
853
- return this.controlTargets[controlKey]
854
- }
855
- return this.deviceValue(field, fallback)
856
- },
857
- async __dispatchControl (command) {
858
- if (command.topic === '__bound') {
859
- const config = this.__boundPanelConfig()
860
- const session = this.__boundPanelSession
861
- if (!config || !session) throw new Error('Bound panel login required')
862
- const response = await fetch(config.endpoint + '/bound-command', {
863
- method: 'POST', credentials: 'same-origin',
864
- headers: { 'Content-Type': 'application/json', 'X-Room-CSRF': session.csrf },
865
- body: JSON.stringify({ room: session.room, _controlPanel: {
866
- kind: 'command', id: command.controlKey, value: command.payload, final: command.final
867
- } }),
868
- signal: AbortSignal.timeout(8000)
869
- })
870
- if (!response.ok) {
871
- if (response.status === 401) this.clearBoundPanelSession()
872
- throw new Error('Bound command failed (' + response.status + ')')
873
- }
874
- return response
875
- }
876
- if (typeof this.controlTransport === 'function') return this.controlTransport(command)
877
-
878
- const session = this.__controlPanelSession
879
- const room = session?.room || this.authorizedRoom
880
- if (typeof this.postControl === 'function') {
881
- return this.postControl(room ? { ...command, room } : command, this.sessionGeneration)
882
- }
883
- if (!session) throw new Error('Call setControlSession() after login, or define controlTransport(command)')
884
-
885
- const response = await fetch('/room-panel-api/regions/' + encodeURIComponent(session.room) + '/command', {
425
+ async __dispatchControl(command) {
426
+ if (command.topic !== '__bound')
427
+ throw new Error('Unsupported control command');
428
+ const config = this.__boundPanelConfig();
429
+ const session = this.__boundPanelSession;
430
+ if (!config || !session)
431
+ throw new Error('Bound panel login required');
432
+ const response = await this.__panelFetch(config.endpoint + '/bound-command', {
886
433
  method: 'POST',
887
434
  credentials: 'same-origin',
888
- headers: { 'Content-Type': 'application/json', 'X-Room-CSRF': session.csrf },
889
- body: JSON.stringify({ room: session.room, ...command }),
890
- signal: AbortSignal.timeout(8000)
891
- })
892
- if (!response.ok) throw new Error('Control command failed (' + response.status + ')')
893
- return response
435
+ headers: {
436
+ 'Content-Type': 'application/json',
437
+ 'X-Room-CSRF': session.csrf
438
+ },
439
+ body: JSON.stringify({
440
+ room: session.room,
441
+ _controlPanel: {
442
+ kind: 'command',
443
+ id: command.controlKey,
444
+ value: command.payload,
445
+ final: command.final
446
+ }
447
+ }),
448
+ }, 8000);
449
+ if (!response.ok) {
450
+ if (response.status === 401)
451
+ this.clearBoundPanelSession();
452
+ throw new Error('Bound command failed (' + response.status + ')');
453
+ }
454
+ return response;
894
455
  }
895
456
  },
896
457
  mounted () {
@@ -1026,11 +587,8 @@ function injectControlPolicy (format, controls = [], boundPanel = null) {
1026
587
 
1027
588
  module.exports = {
1028
589
  CONTROL_POLICY_MARKER,
1029
- FEEDBACK_TOPIC_FIELDS,
1030
- buildControlPolicyMixinSource,
1031
590
  buildDashboardPolicyOptionsSource,
1032
591
  createControlPanelPolicy,
1033
592
  injectControlPolicy,
1034
593
  mergeDashboardOptions,
1035
- normalizeControlFeedback
1036
594
  }