node-red-contrib-room-tools 1.8.1 → 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.
@@ -1,6 +1,6 @@
1
1
  # ControlPanel AI 开发契约
2
2
 
3
- 适用节点:`ui-control-panel` 1.8.0。
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.0 uses the same control semantics at the panel, gateway and device
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
@@ -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.0')
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; applyControlFeedback() never sends commands.')
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
@@ -274,7 +274,6 @@ const __controlPanelPolicyMixin = {
274
274
  },
275
275
  __acknowledgeControlFeedback (feedback) {
276
276
  for (const [key, request] of this.__controlPanelRequests) {
277
- if (feedback.controlKey && feedback.controlKey !== key) continue
278
277
  let matched = request.topic === 'room/reset'
279
278
  let value
280
279
  if (!matched && feedback.topic === request.topic) {
@@ -287,7 +286,15 @@ const __controlPanelPolicyMixin = {
287
286
  value = feedback.state[field]
288
287
  }
289
288
  }
290
- if (matched && (request.topic === 'room/reset' || Object.is(value, request.value))) {
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)) {
291
298
  this.__clearControlPending(key)
292
299
  }
293
300
  }
@@ -503,7 +510,6 @@ export default {
503
510
  __acknowledgeControlFeedback (feedback) {
504
511
  const feedbackFields = this.__controlPanelFeedbackFields()
505
512
  for (const [key, request] of this.__controlPanelRequests) {
506
- if (feedback.controlKey && feedback.controlKey !== key) continue
507
513
  let matched = request.topic === 'room/reset'
508
514
  let value
509
515
  if (!matched && feedback.topic === request.topic) {
@@ -516,7 +522,15 @@ export default {
516
522
  value = feedback.state[field]
517
523
  }
518
524
  }
519
- if (matched && (request.topic === 'room/reset' || Object.is(value, request.value))) {
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)) {
520
534
  this.__clearControlPending(key)
521
535
  }
522
536
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-room-tools",
3
- "version": "1.8.1",
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"],