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_PANEL_AI_GUIDE.md +156 -39
- package/README.md +10 -0
- package/control-catalog.js +7 -6
- package/control-panel.js +5 -5
- package/control-policy.js +36 -478
- package/control-type-model.js +30 -31
- package/gateway.cjs +13 -261
- package/managed-bridge-admin.js +5 -11
- package/native-binding-plan.js +1 -1
- package/npm-shrinkwrap.json +433 -0
- package/package.json +8 -5
- package/panel-binding-editor.js +25 -36
- package/panel-editor-auth.js +3 -39
- package/panel-http.js +69 -0
- package/panel-visual-apply.js +19 -13
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
|
-
? `
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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: {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
}
|