node-red-contrib-homekit-bridged 2.0.0-dev.5 → 2.0.0-dev.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.
Files changed (128) hide show
  1. package/build/lib/HAPHostNode.js +183 -141
  2. package/build/lib/HAPServiceNode.js +199 -172
  3. package/build/lib/HAPServiceNode2.js +207 -172
  4. package/build/lib/NRCHKBError.js +23 -2
  5. package/build/lib/PairingQRCode.js +62 -0
  6. package/build/lib/Storage.js +152 -90
  7. package/build/lib/api.js +654 -288
  8. package/build/lib/camera/CameraControl.js +119 -84
  9. package/build/lib/camera/CameraDelegate.js +481 -404
  10. package/build/lib/camera/MP4StreamingServer.js +148 -139
  11. package/build/lib/hap/HAPCharacteristic.js +25 -4
  12. package/build/lib/hap/HAPService.js +25 -4
  13. package/build/lib/hap/eve-app/EveCharacteristics.js +124 -81
  14. package/build/lib/hap/eve-app/EveServices.js +50 -17
  15. package/build/lib/hap/hap-nodejs.js +32 -0
  16. package/build/lib/migration/HomeKitService2Migration.js +34 -0
  17. package/build/lib/migration/NodeMigration.js +75 -0
  18. package/build/lib/types/AccessoryInformationType.js +15 -1
  19. package/build/lib/types/CameraConfigType.js +15 -1
  20. package/build/lib/types/CustomCharacteristicType.js +15 -1
  21. package/build/lib/types/HAPHostConfigType.js +15 -1
  22. package/build/lib/types/HAPHostNodeType.js +15 -1
  23. package/build/lib/types/HAPService2ConfigType.js +15 -1
  24. package/build/lib/types/HAPService2NodeType.js +15 -1
  25. package/build/lib/types/HAPServiceConfigType.js +15 -1
  26. package/build/lib/types/HAPServiceNodeType.js +15 -1
  27. package/build/lib/types/HAPStatusConfigType.js +15 -1
  28. package/build/lib/types/HAPStatusNodeType.js +15 -1
  29. package/build/lib/types/HostType.js +28 -7
  30. package/build/lib/types/NodeType.js +15 -1
  31. package/build/lib/types/PublishTimersType.js +15 -1
  32. package/build/lib/types/UniFiControllerConfigType.js +16 -0
  33. package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.js +28 -7
  34. package/build/lib/types/hap-nodejs/HapCategories.js +64 -43
  35. package/build/lib/types/storage/SerializedHostType.js +15 -1
  36. package/build/lib/types/storage/StorageType.js +34 -10
  37. package/build/lib/unifi/ProtectDiscovery.js +80 -0
  38. package/build/lib/utils/AccessoryUtils.js +152 -110
  39. package/build/lib/utils/BridgeUtils.js +82 -39
  40. package/build/lib/utils/CharacteristicUtils.js +5 -49
  41. package/build/lib/utils/CharacteristicUtils2.js +5 -49
  42. package/build/lib/utils/CharacteristicUtilsBase.js +81 -0
  43. package/build/lib/utils/NodeStatusUtils.js +89 -40
  44. package/build/lib/utils/ServiceUtils.js +433 -375
  45. package/build/lib/utils/ServiceUtils2.js +519 -309
  46. package/build/lib/utils/index.js +10 -11
  47. package/build/nodes/bridge.html +206 -168
  48. package/build/nodes/bridge.js +27 -9
  49. package/build/nodes/locales/en-US/node-red-contrib-homekit-bridged.json +22 -0
  50. package/build/nodes/nrchkb.html +1753 -117
  51. package/build/nodes/nrchkb.js +66 -88
  52. package/build/nodes/plugin-instance.html +509 -0
  53. package/build/nodes/plugin-instance.js +46 -0
  54. package/build/nodes/service.html +544 -307
  55. package/build/nodes/service.js +5 -6
  56. package/build/nodes/service2.html +1721 -455
  57. package/build/nodes/service2.js +5 -8
  58. package/build/nodes/standalone.html +208 -176
  59. package/build/nodes/standalone.js +27 -9
  60. package/build/nodes/status.html +51 -18
  61. package/build/nodes/status.js +47 -40
  62. package/build/nodes/unifi-controller.html +92 -0
  63. package/build/nodes/unifi-controller.js +20 -0
  64. package/build/plugins/embedded/homebridge-camera-ffmpeg/index.js +479 -0
  65. package/build/plugins/embedded/homebridge-unifi-protect/index.js +521 -0
  66. package/build/plugins/embedded/index.js +58 -0
  67. package/build/plugins/nrchkb-homekit-plugins.js +17 -0
  68. package/build/plugins/registry/index.js +203 -0
  69. package/build/plugins/registry/types.js +16 -0
  70. package/build/scripts/migrate-homekit-service-flows.js +47 -0
  71. package/examples/demo/01 - ALL Demos single import.json +1885 -1885
  72. package/examples/demo/02 - Air Purifier.json +279 -279
  73. package/examples/demo/03 - Air Quality sensor with Battery.json +254 -254
  74. package/examples/demo/04 - Dimmable Bulb.json +172 -172
  75. package/examples/demo/05 - Color Bulb (HSV).json +195 -195
  76. package/examples/demo/06 - Fan (simple, 3 speeds).json +240 -240
  77. package/examples/demo/07 - Fan (with speed, oscillate, rotation direction).json +175 -175
  78. package/examples/demo/08 - CO2 detector.json +224 -224
  79. package/examples/demo/09 - CO (carbon monoxide) example.json +255 -255
  80. package/examples/demo/10 - Door window contact sensor.json +234 -234
  81. package/examples/demos (advanced)/01 - Television with inputs and speaker.json +541 -541
  82. package/examples/switch/01 - Plain Switch.json +178 -178
  83. package/package.json +95 -84
  84. package/build/lib/HAPHostNode.d.ts +0 -1
  85. package/build/lib/HAPServiceNode.d.ts +0 -1
  86. package/build/lib/HAPServiceNode2.d.ts +0 -1
  87. package/build/lib/NRCHKBError.d.ts +0 -3
  88. package/build/lib/Storage.d.ts +0 -30
  89. package/build/lib/api.d.ts +0 -1
  90. package/build/lib/camera/CameraControl.d.ts +0 -3
  91. package/build/lib/camera/CameraDelegate.d.ts +0 -38
  92. package/build/lib/camera/MP4StreamingServer.d.ts +0 -26
  93. package/build/lib/hap/HAPCharacteristic.d.ts +0 -9
  94. package/build/lib/hap/HAPService.d.ts +0 -6
  95. package/build/lib/hap/eve-app/EveCharacteristics.d.ts +0 -20
  96. package/build/lib/hap/eve-app/EveServices.d.ts +0 -5
  97. package/build/lib/types/AccessoryInformationType.d.ts +0 -11
  98. package/build/lib/types/CameraConfigType.d.ts +0 -24
  99. package/build/lib/types/CustomCharacteristicType.d.ts +0 -6
  100. package/build/lib/types/HAPHostConfigType.d.ts +0 -22
  101. package/build/lib/types/HAPHostNodeType.d.ts +0 -14
  102. package/build/lib/types/HAPService2ConfigType.d.ts +0 -6
  103. package/build/lib/types/HAPService2NodeType.d.ts +0 -7
  104. package/build/lib/types/HAPServiceConfigType.d.ts +0 -26
  105. package/build/lib/types/HAPServiceNodeType.d.ts +0 -38
  106. package/build/lib/types/HAPStatusConfigType.d.ts +0 -5
  107. package/build/lib/types/HAPStatusNodeType.d.ts +0 -12
  108. package/build/lib/types/HostType.d.ts +0 -5
  109. package/build/lib/types/NodeType.d.ts +0 -3
  110. package/build/lib/types/PublishTimersType.d.ts +0 -4
  111. package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.d.ts +0 -5
  112. package/build/lib/types/hap-nodejs/HapCategories.d.ts +0 -41
  113. package/build/lib/types/storage/SerializedHostType.d.ts +0 -5
  114. package/build/lib/types/storage/StorageType.d.ts +0 -8
  115. package/build/lib/utils/AccessoryUtils.d.ts +0 -1
  116. package/build/lib/utils/BridgeUtils.d.ts +0 -1
  117. package/build/lib/utils/CharacteristicUtils.d.ts +0 -1
  118. package/build/lib/utils/CharacteristicUtils2.d.ts +0 -1
  119. package/build/lib/utils/NodeStatusUtils.d.ts +0 -17
  120. package/build/lib/utils/ServiceUtils.d.ts +0 -1
  121. package/build/lib/utils/ServiceUtils2.d.ts +0 -1
  122. package/build/lib/utils/index.d.ts +0 -1
  123. package/build/nodes/bridge.d.ts +0 -1
  124. package/build/nodes/nrchkb.d.ts +0 -1
  125. package/build/nodes/service.d.ts +0 -1
  126. package/build/nodes/service2.d.ts +0 -1
  127. package/build/nodes/standalone.d.ts +0 -1
  128. package/build/nodes/status.d.ts +0 -1
@@ -1,326 +1,472 @@
1
1
  <!--suppress EqualityComparisonWithCoercionJS -->
2
- <script data-template-name="homekit-service2" type="text/x-red">
3
- <div class="form-row">
4
- <label for="node-config-input-isParent"><i class="fa fa-tasks"></i> Service Hierarchy</label>
5
- <select id="node-config-input-isParent" style="width: 70%">
6
- <option value="true" selected="selected">Parent</option>
7
- <option value="false">Linked</option>
8
- </select>
9
- </div>
2
+ <style>
3
+ .nrchkb-editor .nrchkb-plugins-summary {
4
+ display: flex !important;
5
+ align-items: center;
6
+ gap: 6px;
7
+ min-width: 0;
8
+ padding-right: 112px;
9
+ }
10
10
 
11
- <div class="form-row">
12
- <label for="node-input-serviceName">
13
- <i class="fa fa-cog"></i>
14
- Service</label>
15
- <select id="node-input-serviceName" style="width: 70%">
16
- <option value="">Choose...</option>
17
- </select>
18
- </div>
11
+ .nrchkb-editor .nrchkb-plugins-summary::-webkit-details-marker {
12
+ display: none;
13
+ }
19
14
 
20
- <div id="isParent">
21
- <div class="form-row">
22
- <label for="node-config-input-hostType"><i class="fa fa-tasks"></i> Host Type</label>
23
- <select id="node-config-input-hostType" style="width: 70%">
24
- <option value="0" selected="selected">Bridge</option>
25
- <option value="1">Accessory</option>
26
- </select>
27
- </div>
28
- <div id="isOnBridge">
29
- <div id="isBridgeInSubflow" class="alert alert-warning" role="alert">
30
- Read more <b><a href="#" id="bridgeInSubflowNotice">here</a></b> about adding Bridge in a Subflow.
31
- </div>
32
- <div class="form-row" style="height: 34px;">
33
- <label for="node-input-bridge">
34
- <i class="fa fa-rocket"></i>
35
- Bridge</label>
36
- <input id="node-input-bridge">
37
- </div>
38
- <div class="form-row">
39
- <label for="node-input-manufacturer"><i class="fa fa-wrench"></i> Manufacturer</label>
40
- <input type="text" id="node-input-manufacturer" placeholder="Manufacturer">
41
- </div>
42
- <div class="form-row">
43
- <label for="node-input-serialNo"><i class="fa fa-wrench"></i> Serial Number</label>
44
- <input type="text" id="node-input-serialNo" placeholder="Serial Number">
15
+ .nrchkb-editor .nrchkb-plugin-add {
16
+ position: absolute;
17
+ top: 5px;
18
+ right: 8px;
19
+ z-index: 1;
20
+ white-space: nowrap;
21
+ }
22
+
23
+ .nrchkb-editor .nrchkb-plugin-label {
24
+ display: flex;
25
+ align-items: baseline;
26
+ gap: 5px;
27
+ min-width: 0;
28
+ overflow: hidden;
29
+ white-space: nowrap;
30
+ }
31
+
32
+ .nrchkb-editor .nrchkb-plugin-count {
33
+ color: var(--red-ui-secondary-text-color, #666);
34
+ font-weight: normal;
35
+ }
36
+
37
+ .nrchkb-editor .nrchkb-plugin-summary-list {
38
+ display: inline;
39
+ min-width: 0;
40
+ overflow: hidden;
41
+ color: var(--red-ui-secondary-text-color, #666);
42
+ font-size: 12px;
43
+ font-weight: normal;
44
+ text-overflow: ellipsis;
45
+ white-space: nowrap;
46
+ }
47
+
48
+ .nrchkb-editor .nrchkb-plugin-empty {
49
+ margin: 0;
50
+ padding: 8px 0;
51
+ color: var(--red-ui-secondary-text-color, #666);
52
+ }
53
+
54
+ .nrchkb-editor .nrchkb-plugin-overview {
55
+ display: grid;
56
+ gap: 6px;
57
+ }
58
+
59
+ .nrchkb-editor .nrchkb-plugin-overview-item {
60
+ display: grid;
61
+ grid-template-columns: 1fr auto;
62
+ align-items: center;
63
+ gap: 8px;
64
+ padding: 5px 8px;
65
+ border: 1px solid var(--red-ui-secondary-border-color, #d8d8d8);
66
+ border-radius: 6px;
67
+ background: var(--red-ui-primary-background, #fff);
68
+ }
69
+
70
+ .nrchkb-editor .nrchkb-plugin-overview-name {
71
+ min-width: 0;
72
+ overflow: hidden;
73
+ text-overflow: ellipsis;
74
+ white-space: nowrap;
75
+ }
76
+
77
+ .nrchkb-editor .nrchkb-plugin-overview-name {
78
+ font-weight: 600;
79
+ }
80
+
81
+ .nrchkb-editor .nrchkb-plugin-overview-actions {
82
+ display: flex;
83
+ gap: 6px;
84
+ flex-shrink: 0;
85
+ }
86
+
87
+ .nrchkb-editor .nrchkb-plugin-section {
88
+ margin-top: 8px;
89
+ }
90
+
91
+ .nrchkb-editor .nrchkb-plugin-shell {
92
+ position: relative;
93
+ }
94
+
95
+ .nrchkb-editor .nrchkb-plugin-section-toolbar {
96
+ display: flex;
97
+ justify-content: flex-end;
98
+ margin: 0 0 8px;
99
+ }
100
+
101
+ .nrchkb-editor .nrchkb-plugin-section summary {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 6px;
105
+ min-width: 0;
106
+ list-style: none;
107
+ }
108
+
109
+ .nrchkb-editor .nrchkb-plugin-section summary::-webkit-details-marker {
110
+ display: none;
111
+ }
112
+
113
+ .nrchkb-editor .nrchkb-plugin-title {
114
+ flex: 1 1 auto;
115
+ min-width: 0;
116
+ overflow: hidden;
117
+ text-overflow: ellipsis;
118
+ white-space: nowrap;
119
+ }
120
+
121
+ .nrchkb-editor .nrchkb-plugin-remove {
122
+ justify-self: end;
123
+ }
124
+
125
+ .nrchkb-editor .nrchkb-plugin-about {
126
+ margin-bottom: 10px;
127
+ padding: 7px 9px;
128
+ border: 1px solid var(--red-ui-secondary-border-color, #d8d8d8);
129
+ border-radius: 6px;
130
+ background: var(--red-ui-tertiary-background, #f7f7f7);
131
+ color: var(--red-ui-secondary-text-color, #666);
132
+ font-size: 12px;
133
+ line-height: 1.35;
134
+ }
135
+
136
+ .nrchkb-editor .nrchkb-plugin-about strong {
137
+ display: block;
138
+ color: var(--red-ui-primary-text-color, #333);
139
+ }
140
+
141
+ .nrchkb-plugin-picker {
142
+ display: grid;
143
+ gap: 8px;
144
+ }
145
+
146
+ .nrchkb-plugin-picker-item {
147
+ display: block;
148
+ width: 100%;
149
+ padding: 9px 10px;
150
+ border: 1px solid var(--red-ui-secondary-border-color, #d0d0d0);
151
+ border-radius: 6px;
152
+ background: var(--red-ui-primary-background, #fff);
153
+ color: var(--red-ui-primary-text-color, #333);
154
+ cursor: pointer;
155
+ font: inherit;
156
+ text-align: left;
157
+ }
158
+
159
+ .nrchkb-plugin-picker-item:not(:disabled):hover,
160
+ .nrchkb-plugin-picker-item:not(:disabled):focus-visible {
161
+ border-color: var(--red-ui-focus-color, var(--red-ui-text-color-link, #1a73e8));
162
+ }
163
+
164
+ .nrchkb-plugin-picker-item:disabled {
165
+ cursor: default;
166
+ opacity: 0.55;
167
+ }
168
+
169
+ .nrchkb-plugin-picker-title {
170
+ display: flex;
171
+ justify-content: space-between;
172
+ gap: 8px;
173
+ font-weight: 600;
174
+ }
175
+
176
+ .nrchkb-plugin-picker-description,
177
+ .nrchkb-plugin-picker-meta {
178
+ margin-top: 3px;
179
+ color: var(--red-ui-secondary-text-color, #666);
180
+ font-size: 12px;
181
+ line-height: 1.35;
182
+ }
183
+
184
+ .nrchkb-plugin-picker-description {
185
+ display: -webkit-box;
186
+ -webkit-line-clamp: 2;
187
+ -webkit-box-orient: vertical;
188
+ overflow: hidden;
189
+ }
190
+ </style>
191
+
192
+ <script data-template-name="homekit-service2" type="text/x-red">
193
+ <div class="nrchkb-editor">
194
+ <details id="plugins-configuration" class="nrchkb-section nrchkb-plugin-shell">
195
+ <summary class="nrchkb-plugins-summary">
196
+ <i class="fa fa-plug"></i>
197
+ <span class="nrchkb-plugin-label">
198
+ Plugins <span id="node-input-plugin-count" class="nrchkb-plugin-count">0</span>
199
+ <span id="node-input-plugin-summary-list" class="nrchkb-plugin-summary-list">No plugins attached</span>
200
+ </span>
201
+ </summary>
202
+ <button type="button" id="node-input-add-plugin" class="red-ui-button red-ui-button-small nrchkb-plugin-add">
203
+ <i class="fa fa-plus"></i>
204
+ Add plugin
205
+ </button>
206
+ <div class="nrchkb-section-body">
207
+ <div class="nrchkb-plugin-slot-fields" style="display: none;">
208
+ <input type="hidden" id="node-input-plugins">
209
+ <input type="hidden" id="node-input-plugin1">
210
+ <input type="hidden" id="node-input-plugin2">
211
+ <input type="hidden" id="node-input-plugin3">
212
+ <input type="hidden" id="node-input-plugin4">
213
+ <input type="hidden" id="node-input-plugin5">
214
+ <input type="hidden" id="node-input-plugin6">
215
+ <input type="hidden" id="node-input-plugin7">
216
+ <input type="hidden" id="node-input-plugin8">
217
+ </div>
218
+ <div id="node-input-plugin-overview" class="nrchkb-plugin-overview"></div>
45
219
  </div>
46
- <div class="form-row">
47
- <label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
48
- <input type="text" id="node-input-model" placeholder="Model">
220
+ </details>
221
+
222
+ <details class="nrchkb-section" open>
223
+ <summary><i class="fa fa-sliders"></i> Essentials</summary>
224
+ <div class="nrchkb-section-body">
225
+ <div class="form-row">
226
+ <label for="node-config-input-isParent"><i class="fa fa-sitemap"></i> Service Hierarchy</label>
227
+ <select id="node-config-input-isParent">
228
+ <option value="true" selected="selected">Parent</option>
229
+ <option value="false">Linked</option>
230
+ </select>
231
+ </div>
232
+
233
+ <div class="form-row">
234
+ <label for="node-input-serviceName">
235
+ <i class="fa fa-puzzle-piece"></i>
236
+ Service
237
+ </label>
238
+ <select id="node-input-serviceName">
239
+ <option value="">Choose...</option>
240
+ </select>
241
+ </div>
242
+
243
+ <div class="form-row">
244
+ <label for="node-input-name">
245
+ <i class="fa fa-tag"></i>
246
+ Name
247
+ </label>
248
+ <input type="text" id="node-input-name" placeholder="Name">
249
+ </div>
49
250
  </div>
50
- <div class="form-row">
51
- <label for="node-input-firmwareRev"><i class="fa fa-wrench"></i> Firmware Revision</label>
52
- <input type="text" id="node-input-firmwareRev" placeholder="Firmware Revision">
251
+ </details>
252
+
253
+ <details class="nrchkb-section" open>
254
+ <summary><i class="fa fa-map-marker"></i> Placement</summary>
255
+ <div class="nrchkb-section-body">
256
+ <div id="isParent">
257
+ <div class="form-row">
258
+ <label for="node-config-input-hostType"><i class="fa fa-server"></i> Host Type</label>
259
+ <select id="node-config-input-hostType">
260
+ <option value="0" selected="selected">Bridge</option>
261
+ <option value="1">Accessory</option>
262
+ </select>
263
+ </div>
264
+ <div id="isOnBridge">
265
+ <div id="isBridgeInSubflow" class="alert alert-warning nrchkb-warning" role="alert">
266
+ Read more <b><a href="#" id="bridgeInSubflowNotice">here</a></b> about adding Bridge in a Subflow.
267
+ </div>
268
+ <div class="form-row">
269
+ <label for="node-input-bridge">
270
+ <i class="fa fa-link"></i>
271
+ Bridge
272
+ </label>
273
+ <input id="node-input-bridge">
274
+ </div>
275
+ <details class="nrchkb-section">
276
+ <summary><i class="fa fa-id-card-o"></i> Accessory Metadata</summary>
277
+ <div class="nrchkb-section-body">
278
+ <div class="form-row">
279
+ <label for="node-input-manufacturer"><i class="fa fa-industry"></i> Manufacturer</label>
280
+ <input type="text" id="node-input-manufacturer" placeholder="Manufacturer">
281
+ </div>
282
+ <div class="form-row">
283
+ <label for="node-input-serialNo"><i class="fa fa-barcode"></i> Serial Number</label>
284
+ <input type="text" id="node-input-serialNo" placeholder="Serial Number">
285
+ </div>
286
+ <div class="form-row">
287
+ <label for="node-input-model"><i class="fa fa-cube"></i> Model</label>
288
+ <input type="text" id="node-input-model" placeholder="Model">
289
+ </div>
290
+ <div class="form-row">
291
+ <label for="node-input-firmwareRev"><i class="fa fa-microchip"></i> Firmware Revision</label>
292
+ <input type="text" id="node-input-firmwareRev" placeholder="Firmware Revision">
293
+ </div>
294
+ <div class="form-row">
295
+ <label for="node-input-hardwareRev"><i class="fa fa-hdd-o"></i> Hardware Revision</label>
296
+ <input type="text" id="node-input-hardwareRev" placeholder="Hardware Revision">
297
+ </div>
298
+ <div class="form-row">
299
+ <label for="node-input-softwareRev"><i class="fa fa-code"></i> Software Revision</label>
300
+ <input type="text" id="node-input-softwareRev" placeholder="Software Revision">
301
+ </div>
302
+ </div>
303
+ </details>
304
+ </div>
305
+ <div id="isAccessory">
306
+ <div class="form-row">
307
+ <label for="node-input-accessoryId">
308
+ <i class="fa fa-home"></i>
309
+ Accessory
310
+ </label>
311
+ <input id="node-input-accessoryId">
312
+ </div>
313
+ </div>
314
+ </div>
315
+
316
+ <div id="isLinked" style="display: none;">
317
+ <div class="form-row">
318
+ <label for="node-input-parentService">
319
+ <i class="fa fa-level-up"></i>
320
+ Parent Service
321
+ </label>
322
+ <select id="node-input-parentService">
323
+ <option value="">Choose...</option>
324
+ </select>
325
+ </div>
326
+ </div>
53
327
  </div>
54
- <div class="form-row">
55
- <label for="node-input-hardwareRev"><i class="fa fa-wrench"></i> Hardware Revision</label>
56
- <input type="text" id="node-input-hardwareRev" placeholder="Hardware Revision">
328
+ </details>
329
+
330
+ <details class="nrchkb-section" open>
331
+ <summary><i class="fa fa-exchange"></i> Message Routing</summary>
332
+ <div class="nrchkb-section-body">
333
+ <div class="form-row">
334
+ <label for="node-input-topic"><i class="fa fa-comment-o"></i> Topic</label>
335
+ <input type="text" id="node-input-topic" placeholder="Topic">
336
+ </div>
337
+ <div class="form-row nrchkb-checkbox-row">
338
+ <label class="nrchkb-checkbox-label" for="node-input-filter">
339
+ <input type="checkbox" id="node-input-filter">
340
+ <span><i class="fa fa-filter"></i> Filter on Topic</span>
341
+ </label>
342
+ </div>
343
+ <div class="form-row">
344
+ <label for="node-input-outputMode"><i class="fa fa-random"></i> Output mode</label>
345
+ <select id="node-input-outputMode">
346
+ <option value="events" selected="selected">Events</option>
347
+ <option value="legacy">Legacy onChange/onSet</option>
348
+ </select>
349
+ </div>
57
350
  </div>
58
- <div class="form-row">
59
- <label for="node-input-softwareRev"><i class="fa fa-wrench"></i> Software Revision</label>
60
- <input type="text" id="node-input-softwareRev" placeholder="Software Revision">
351
+ </details>
352
+
353
+ <details id="adaptive-lightning-configuration" class="nrchkb-section" open style="display: none;">
354
+ <summary><i class="fa fa-sun-o"></i> Adaptive Lighting Configuration</summary>
355
+ <div class="nrchkb-section-body">
356
+ <div class="form-row nrchkb-checkbox-row">
357
+ <label class="nrchkb-checkbox-label" for="node-input-adaptiveLightingOptionsEnable">
358
+ <input type="checkbox" id="node-input-adaptiveLightingOptionsEnable">
359
+ <span><i class="fa fa-toggle-on"></i> Enable</span>
360
+ </label>
361
+ </div>
362
+ <div class="form-row">
363
+ <label for="node-input-adaptiveLightingOptionsMode"><i class="fa fa-magic"></i> Mode</label>
364
+ <select id="node-input-adaptiveLightingOptionsMode">
365
+ <option value="" selected hidden disabled>AUTOMATIC</option>
366
+ <option value="1">AUTOMATIC</option>
367
+ <option value="2">MANUAL</option>
368
+ </select>
369
+ </div>
370
+ <div class="form-row">
371
+ <label for="node-input-adaptiveLightingOptionsCustomTemperatureAdjustment"><i class="fa fa-thermometer-half"></i> Custom Temperature Adjustment</label>
372
+ <input type="number" id="node-input-adaptiveLightingOptionsCustomTemperatureAdjustment" placeholder="0">
373
+ </div>
61
374
  </div>
62
- </div>
63
- <div id="isAccessory">
64
- <div class="form-row" style="height: 34px;">
65
- <label for="node-input-accessoryId">
66
- <i class="fa fa-rocket"></i>
67
- Accessory</label>
68
- <input id="node-input-accessoryId">
375
+ </details>
376
+
377
+ <details class="nrchkb-section">
378
+ <summary><i class="fa fa-sliders"></i> Advanced Behavior</summary>
379
+ <div class="nrchkb-section-body">
380
+ <div class="form-row">
381
+ <label for="node-input-characteristicProperties"><i class="fa fa-list-alt"></i> Characteristic Properties</label>
382
+ <input type="text" id="node-input-characteristicProperties">
383
+ </div>
384
+
385
+ <div class="form-row nrchkb-checkbox-row">
386
+ <label class="nrchkb-checkbox-label" for="node-input-waitForSetupMsg">
387
+ <input type="checkbox" id="node-input-waitForSetupMsg">
388
+ <span><i class="fa fa-hourglass-half"></i> Wait for Setup message</span>
389
+ </label>
390
+ </div>
391
+
392
+ <div class="form-row nrchkb-checkbox-row">
393
+ <label class="nrchkb-checkbox-label" for="node-input-useEventCallback">
394
+ <input type="checkbox" id="node-input-useEventCallback">
395
+ <span><i class="fa fa-code-fork"></i> Use Event callback</span>
396
+ </label>
397
+ </div>
69
398
  </div>
70
- </div>
71
- </div>
399
+ </details>
72
400
 
73
- <div id="isLinked" style="display: none;">
74
- <div class="form-row">
75
- <label for="node-input-parentService">
76
- <i class="fa fa-cog"></i>
77
- Parent Service</label>
78
- <select id="node-input-parentService">
79
- <option value="">Choose...</option>
80
- </select>
81
- </div>
401
+ <div id="node-input-plugin-sections"></div>
82
402
  </div>
403
+ </script>
83
404
 
84
- <div class="form-row">
85
- <label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
86
- <input type="text" id="node-input-topic" placeholder="Topic">
87
- </div>
88
- <div class="form-row">
89
- <label class="visibleDesktop">&nbsp;</label>
90
- <div style="display: inline;">
91
- <input type="checkbox" id="node-input-filter" style="display: inline-block; width: auto; vertical-align: top;">
92
- <label for="node-input-filter" style="width: 120px;">&nbsp;&nbsp;<i class="fa fa-filter"></i> Filter on Topic</label>
93
- </div>
94
- </div>
405
+ <script data-help-name="homekit-service2" type="text/markdown">
406
+ # HomeKit Service 2
95
407
 
96
- <div class="form-row">
97
- <label for="node-input-name">
98
- <i class="fa fa-tag"></i>
99
- Name</label>
100
- <input type="text" id="node-input-name" placeholder="Name">
101
- </div>
408
+ Represents one HomeKit service, such as a Switch, Outlet, Lightbulb, Sensor, or Camera. It receives Node-RED messages to update HomeKit characteristics and emits HomeKit events from the selected service.
102
409
 
103
- <div id="camera-configuration" style="display: none;">
104
- <label>&nbsp;&nbsp;<i class="fa fa-video-camera"></i> Camera Configuration</label>
105
- <div class="form-row">
106
- <label for="node-input-cameraConfigVideoProcessor"><i class="fa fa-cog"></i> Video Processor</label>
107
- <input type="text" id="node-input-cameraConfigVideoProcessor" placeholder="ffmpeg">
108
- </div>
109
- <div class="form-row">
110
- <label for="node-input-cameraConfigSource"><i class="fa fa-tint"></i> Source</label>
111
- <input type="text" id="node-input-cameraConfigSource" placeholder="">
112
- </div>
113
- <div class="form-row">
114
- <label for="node-input-cameraConfigStillImageSource"><i class="fa fa-picture-o"></i> Still Image Source</label>
115
- <input type="text" id="node-input-cameraConfigStillImageSource" placeholder="">
116
- </div>
117
- <div class="form-row">
118
- <label for="node-input-cameraConfigMaxStreams"><i class="fa fa-tint"></i> Max Streams</label>
119
- <input type="text" id="node-input-cameraConfigMaxStreams" placeholder="">
120
- </div>
121
- <div class="form-row">
122
- <label for="node-input-cameraConfigMaxWidth"><i class="fa fa-text-width"></i> Max Width</label>
123
- <input type="text" id="node-input-cameraConfigMaxWidth" placeholder="">
124
- </div>
125
- <div class="form-row">
126
- <label for="node-input-cameraConfigMaxHeight"><i class="fa fa-text-height"></i> Max Height</label>
127
- <input type="text" id="node-input-cameraConfigMaxHeight" placeholder="">
128
- </div>
129
- <div class="form-row">
130
- <label for="node-input-cameraConfigMaxFPS"><i class="fa fa-clock-o"></i> Max FPS</label>
131
- <input type="text" id="node-input-cameraConfigMaxFPS" placeholder="">
132
- </div>
133
- <div class="form-row">
134
- <label for="node-input-cameraConfigMaxBitrate"><i class="fa fa-clock-o"></i> Max Bitrate</label>
135
- <input type="text" id="node-input-cameraConfigMaxBitrate" placeholder="">
136
- </div>
137
- <div class="form-row">
138
- <label for="node-input-cameraConfigVideoCodec"><i class="fa fa-video-camera"></i> Video Codec</label>
139
- <input type="text" id="node-input-cameraConfigVideoCodec" placeholder="">
140
- </div>
141
- <div class="form-row">
142
- <label for="node-input-cameraConfigAudioCodec"><i class="fa fa-video-camera"></i> Audio Codec</label>
143
- <input type="text" id="node-input-cameraConfigAudioCodec" placeholder="">
144
- </div>
145
- <div class="form-row">
146
- <label for="node-input-cameraConfigAudio"><i class="fa fa-headphones"></i> Audio</label>
147
- <input type="checkbox" id="node-input-cameraConfigAudio">
148
- </div>
149
- <div class="form-row">
150
- <label for="node-input-cameraConfigPacketSize"><i class="fa fa-get-pocket"></i> Packet Size</label>
151
- <input type="text" id="node-input-cameraConfigPacketSize" placeholder="">
152
- </div>
153
- <div class="form-row">
154
- <label for="node-input-cameraConfigVerticalFlip"><i class="fa fa-undo"></i> Vertical Flip</label>
155
- <input type="checkbox" id="node-input-cameraConfigVerticalFlip">
156
- </div>
157
- <div class="form-row">
158
- <label for="node-input-cameraConfigHorizontalFlip"><i class="fa fa-undo"></i> Horizontal Flip</label>
159
- <input type="checkbox" id="node-input-cameraConfigHorizontalFlip">
160
- </div>
161
- <div class="form-row">
162
- <label for="node-input-cameraConfigMapVideo"><i class="fa fa-video-camera"></i> Map Video</label>
163
- <input type="text" id="node-input-cameraConfigMapVideo" placeholder="">
164
- </div>
165
- <div class="form-row">
166
- <label for="node-input-cameraConfigMapAudio"><i class="fa fa-headphones"></i> Map Audio</label>
167
- <input type="text" id="node-input-cameraConfigMapAudio" placeholder="">
168
- </div>
169
- <div class="form-row">
170
- <label for="node-input-cameraConfigVideoFilter"><i class="fa fa-filter"></i> Video Filter</label>
171
- <input type="text" id="node-input-cameraConfigVideoFilter" placeholder="">
172
- </div>
173
- <div class="form-row">
174
- <label for="node-input-cameraConfigAdditionalCommandLine"><i class="fa fa-plus"></i> Additional Command Line</label>
175
- <input type="text" id="node-input-cameraConfigAdditionalCommandLine" placeholder="">
176
- </div>
177
- <div class="form-row">
178
- <label for="node-input-cameraConfigDebug"><i class="fa fa-bug"></i> Debug</label>
179
- <input type="checkbox" id="node-input-cameraConfigDebug">
180
- </div>
181
-
182
- <div class="form-row">
183
- <label for="node-input-cameraConfigSnapshotOutput"><i class="fa fa-picture-o"></i> Snapshot output</label>
184
- <select id="node-input-cameraConfigSnapshotOutput">
185
- <option value="disabled" selected="selected">Disabled</option>
186
- <option value="path">Path</option>
187
- <option value="content">Content</option>
188
- </select>
189
- </div>
190
-
191
- <div class="form-row">
192
- <label for="node-input-cameraConfigInterfaceName"><i class="fa fa-smile-o"></i> Interface Name</label>
193
- <input type="text" id="node-input-cameraConfigInterfaceName" placeholder="">
194
- </div>
195
- </div>
410
+ > [!IMPORTANT]
411
+ > Input payload keys must be HomeKit characteristic names. For example, send `{"On": true}` to turn on a Switch or Outlet.
196
412
 
197
- <div id="adaptive-lightning-configuration" style="display: none; border: 1px solid var(--red-ui-secondary-border-color); padding: 12px 12px 0 12px; margin-bottom: 12px;">
198
- <label>&nbsp;&nbsp;<i class="fa fa-cog"></i> Adaptive Lightning Configuration</label>
199
- <div class="form-row">
200
- <label for="node-input-adaptiveLightingOptionsEnable"><i class="fa fa-toggle-on"></i> Enable</label>
201
- <input type="checkbox" id="node-input-adaptiveLightingOptionsEnable">
202
- </div>
203
- <div class="form-row">
204
- <label for="node-input-adaptiveLightingOptionsMode"><i class="fa fa-hand-o-up"></i> Mode</label>
205
- <select id="node-input-adaptiveLightingOptionsMode">
206
- <option value="" selected hidden disabled>AUTOMATIC</option>
207
- <option value="1">AUTOMATIC</option>
208
- <option value="2">MANUAL</option>
209
- </select>
210
- </div>
211
- <div class="form-row">
212
- <label for="node-input-adaptiveLightingOptionsCustomTemperatureAdjustment"><i class="fa fa-thermometer-quarter"></i> Custom Temperature Adjustment</label>
213
- <input type="number" id="node-input-adaptiveLightingOptionsCustomTemperatureAdjustment" placeholder="0">
214
- </div>
215
- </div>
413
+ > [!NOTE]
414
+ > **Events** output mode is the preferred layout for new flows. **Legacy onChange/onSet** is available for migrated `homekit-service` flows.
216
415
 
217
- <div class="form-row">
218
- <label for="node-input-characteristicProperties"><i class="fa fa-wrench"></i> Characteristic Properties</label>
219
- <input type="text" id="node-input-characteristicProperties" style="width: 70%">
220
- </div>
416
+ ## Essentials
221
417
 
222
- <div class="form-row">
223
- <label for="node-input-waitForSetupMsg"><i class="fa fa-bug"></i> Wait for Setup message</label>
224
- <input type="checkbox" id="node-input-waitForSetupMsg">
225
- </div>
418
+ - **Service Hierarchy**: Select **Parent** for the main service of an accessory, or **Linked** for a secondary service attached to another service.
419
+ - **Service**: HAP service type exposed to HomeKit.
420
+ - **Name**: Service name shown in HomeKit and used as the default topic match when topic filtering is enabled without a configured topic.
226
421
 
227
- <div class="form-row">
228
- <label for="node-input-useEventCallback"><i class="fa fa-code-fork"></i> Use Event callback</label>
229
- <input type="checkbox" id="node-input-useEventCallback">
230
- </div>
231
- </script>
422
+ ## Placement
232
423
 
233
- <script data-help-name="homekit-service2" type="text/x-red">
234
- <h3 id="toc_5">Service</h3>
235
- <p>The Service node represents the single device you want to control or query.</p>
236
- <p>Every service node creates its own HAP accessory to keep things simple.</p>
237
- <ul>
238
- <li><strong>Service Hierarchy</strong>: Choose if this Service is Parent or Linked</li>
239
- <ul>
240
- <li>Parent has <strong>Bridge</strong> and <strong>Accessory Category</strong>: On what Bridge to host this Service and its Accessory and what kind of category is this Accessory, default <em>OTHER</em></li>
241
- <li>Linked has <strong>Parent Service</strong>: On what Parent Service link this Service.</li>
242
- </ul>
243
- <li><strong>Topic</strong>: An optional property that can be configured in the node or, if left blank, can be set by <code>msg.topic</code>. If <em>Filter on Topic</em> is selected <code>msg.topic</code> of incoming messages must match the configured
244
- value for the message to be accepted. If <em>Filter on Topic</em> is selected and no <em>Topic</em> is set on the node, then <code>msg.topic</code> must match the nodes <em>Name</em>
245
- </li>
246
- <li><strong>Manufacturer, Model, Serial Number</strong>: Can be anything you want.</li>
247
- <li><strong>Firmware Revision</strong>: Should be a version number string in the form of <em>MAJOR.MINOR.REVISION</em> e.g. <em>1.2.0</em>. Other types of strings are ignored and won't be displayed.</li>
248
- <li><strong>Hardware Revision</strong>: Should be a version number string in the form of <em>MAJOR.MINOR.REVISION</em> e.g. <em>1.2.0</em>. Other types of strings are ignored and won't be displayed.</li>
249
- <li><strong>Software Revision</strong>: Should be a version number string in the form of <em>MAJOR.MINOR.REVISION</em> e.g. <em>1.2.0</em>. Other types of strings are ignored and won't be displayed.</li>
250
- <li><strong>Service</strong>: Choose the type of Service from the list.</li>
251
- <li><strong>Name</strong>: <em>optional</em></li>
252
- <li><strong>Camera Configuration</strong>: Additional configuration for CameraControl service.</li>
253
- <ul>
254
- <li><strong>Video Processor</strong>: Video processor used for Camera. Default is <em>ffmpeg</em>.</li>
255
- <li><strong>Source</strong>: Camera source used for video processor. Example for ffmpeg <em>-re -i rtsp://192.168.0.227:8554/unicast</em></li>
256
- <li><strong>Still Image Source</strong>: Camera snapshot source used for video processor. Example for ffmpeg <em>-i http://faster_still_image_grab_url/this_is_optional.jpg</em></li>
257
- <li><strong>Max Streams</strong>: Maximum number of streams that will be generated for this camera, default <em>2</em>.</li>
258
- <li><strong>Max Width</strong>: Maximum width reported to HomeKit, default <em>1280</em>.</li>
259
- <li><strong>Max Height</strong>: Maximum height reported to HomeKit, default <em>720</em>.</li>
260
- <li><strong>Max FPS</strong>: Maximum frame rate of the stream, default <em>10</em>.</li>
261
- <li><strong>Max Bitrate</strong>: Maximum bit rate of the stream in kbit/s, default <em>300</em>.</li>
262
- <li><strong>Video Codec</strong>: If you're running on a RPi with the omx version of ffmpeg installed, you can change to the hardware accelerated video codec with this option, default <em>libx264</em>.</li>
263
- <li><strong>Audio Codec</strong>: If you're running on a RPi with the omx version of ffmpeg installed, you can change to the hardware accelerated audio codec with this option, default <em>libfdk_aac</em>.</li>
264
- <li><strong>Audio</strong>: Can be set to true to enable audio streaming from camera. To use audio ffmpeg must be compiled with --enable-libfdk-aac, default <em>false</em>.</li>
265
- <li><strong>Packet Size</strong>: If audio or video is choppy try a smaller value, set to a multiple of 188, default <em>1316</em>.</li>
266
- <li><strong>Vertical Flip</strong>: Flips the stream vertically, default <em>false</em>.</li>
267
- <li><strong>Horizontal Flip</strong>: Flips the stream horizontally, default <em>false</em>.</li>
268
- <li><strong>Map Video</strong>: Select the stream used for video, default <em>0:0</em>.</li>
269
- <li><strong>Map Audio</strong>: Select the stream used for audio, default <em>0:1</em>.</li>
270
- <li><strong>Video Filter</strong>: Allows a custom video filter to be passed to FFmpeg via -vf, defaults to <em>scale=1280:720</em> but is optional.</li>
271
- <li><strong>Additional Command Line</strong>: Allows additional of extra command line options to FFmpeg, default <em>-tune zerolatency</em> but is optional.</li>
272
- <li><strong>Debug</strong>: Show the output of ffmpeg in the log, default <em>false</em>.</li>
273
- <li><strong>Snapshot output</strong>: Choose how to output camera snapshot</li>
274
- <ul>
275
- <li><strong>Disabled</strong>: there will be no output</li>
276
- <li><strong>Path</strong>: file will be saved and path will be send to output, <em>msg.payload.cameraSnapshot</em> contains path value stored as a string.</li>
277
- <li><strong>Content</strong>: file content will be send to output, <em>msg.payload.cameraSnapshot</em> contains Buffer object {"type":"Buffer","data":[]}.</li>
278
- </ul>
279
- <li><strong>Interface Name</strong>: Selects the IP address of a given network interface.</li>
280
- </ul>
281
- <li><strong>Characteristic Properties</strong>: Customize the properties of characteristics.</li>
282
- <li><strong>Wait for Setup message</strong>: If yes then Service node will wait for a input message with appropriate payload.</li>
283
- <li><strong>Use Event callback</strong>: If yes then Service node will wait for a callback message to respond to get event.</li>
284
- </ul>
285
- <h2 id="toc_6">Input Messages</h2>
286
- <p>Input messages can be used to update any <em>Characteristic</em> that the selected <em>Service</em> provides. Simply pass the values-to-update as <code>msg.payload</code> object. </p>
287
- <p><strong>Example</strong>: to signal that an <em>Outlet</em> is turned on and in use, send the following payload</p>
288
- <div>
289
- <pre><code class="language-javascript">
290
- {
291
- &quot;On&quot;: 1,
292
- &quot;OutletInUse&quot;: 1
293
- }
294
- </code></pre>
295
- </div>
296
- <p><strong>Hint</strong>: to find out what <em>Characteristics</em> you can address, just send <code>{&quot;foo&quot;:&quot;bar&quot;}</code> and watch the debug tab ;)</p>
297
- <h2 id="toc_7">Output Messages</h2>
298
- <p>Output messages are in the same format as input messages. They are emitted from the node when it receives <em>Characteristics</em> updates from a paired iOS device.</p>
299
- <h2 id="toc_8">Characteristic Properties</h2>
300
- <p><strong>Example</strong>: allow temperatures below 0&deg;C</p>
301
- <div>
302
- <pre><code class="language-json">
424
+ - **Host Type**: Publish a parent service behind a shared **Bridge** or behind a standalone **Accessory**.
425
+ - **Bridge**: HomeKit bridge configuration that hosts this service.
426
+ - **Accessory**: Standalone accessory configuration that hosts this service.
427
+ - **Parent Service**: Parent service used by linked services.
428
+
429
+ ## Message Routing
430
+
431
+ - **Topic**: Optional route key. If blank, incoming messages may provide `msg.topic`.
432
+ - **Filter on Topic**: Accept incoming messages only when `msg.topic` matches the configured Topic. If Topic is blank, `msg.topic` must match the node Name.
433
+ - **Output mode**: **Events** emits all HomeKit events through one output; **Legacy onChange/onSet** preserves the old output layout.
434
+
435
+ ## Plugins
436
+
437
+ Plugins add service-specific behavior and render their own configuration fields from registered plugin metadata.
438
+
439
+ ## Advanced Behavior
440
+
441
+ > [!WARNING]
442
+ > **Wait for Setup message** delays service setup until the node receives the required setup payload. The service will not be published until that message arrives.
443
+
444
+ - **Characteristic Properties**: JSON object that overrides HAP characteristic metadata such as `minValue`, `maxValue`, or `minStep`.
445
+ - **Use Event callback**: Lets a flow answer HomeKit read/get events by sending a callback response message.
446
+
447
+ ## Input Messages
448
+
449
+ ```json
303
450
  {
304
- &quot;CurrentTemperature&quot;: {
305
- &quot;minValue&quot;: -100
306
- }
451
+ "On": true,
452
+ "OutletInUse": true
307
453
  }
308
- </code></pre>
309
- <p><strong>Example</strong>: limit fan speed multiples of 25%</p>
310
- <div>
311
- <pre><code class="language-json">
454
+ ```
455
+
456
+ To discover valid characteristic names for the selected service, send a test payload such as `{"foo":"bar"}` and check the Node-RED debug output.
457
+
458
+ ## Characteristic Properties
459
+
460
+ ```json
312
461
  {
313
- &quot;RotationSpeed&quot;: {
314
- &quot;minStep&quot;: 25
315
- }
462
+ "CurrentTemperature": {
463
+ "minValue": -100
464
+ }
316
465
  }
317
- </code></pre>
318
- </div>
319
-
466
+ ```
320
467
  </script>
321
468
 
322
469
  <script type="text/javascript">
323
- if (nrchkbExperimental) {
324
470
  RED.nodes.registerType('homekit-service2', {
325
471
  category: "Apple HomeKit",
326
472
  paletteLabel: 'service 2',
@@ -404,81 +550,6 @@ if (nrchkbExperimental) {
404
550
  required: false,
405
551
  validate: versionValidator,
406
552
  },
407
- cameraConfigVideoProcessor: {
408
- value: 'ffmpeg',
409
- validate: cameraConfigRequiredField,
410
- },
411
- cameraConfigSource: {
412
- validate: cameraConfigRequiredField,
413
- },
414
- cameraConfigStillImageSource: {
415
- required: false,
416
- },
417
- cameraConfigMaxStreams: {
418
- value: 2,
419
- validate: cameraConfigRequiredField,
420
- },
421
- cameraConfigMaxWidth: {
422
- value: 1280,
423
- validate: cameraConfigRequiredField,
424
- },
425
- cameraConfigMaxHeight: {
426
- value: 720,
427
- validate: cameraConfigRequiredField,
428
- },
429
- cameraConfigMaxFPS: {
430
- value: 10,
431
- validate: cameraConfigRequiredField,
432
- },
433
- cameraConfigMaxBitrate: {
434
- value: 300,
435
- validate: cameraConfigRequiredField,
436
- },
437
- cameraConfigVideoCodec: {
438
- value: 'libx264',
439
- validate: cameraConfigRequiredField,
440
- },
441
- cameraConfigAudioCodec: {
442
- value: 'libfdk_aac',
443
- validate: cameraConfigRequiredField,
444
- },
445
- cameraConfigAudio: {
446
- value: false,
447
- },
448
- cameraConfigPacketSize: {
449
- value: 1316,
450
- validate: cameraConfigRequiredField,
451
- },
452
- cameraConfigVerticalFlip: {
453
- value: false,
454
- },
455
- cameraConfigHorizontalFlip: {
456
- value: false,
457
- },
458
- cameraConfigMapVideo: {
459
- value: '0:0',
460
- validate: cameraConfigRequiredField,
461
- },
462
- cameraConfigMapAudio: {
463
- value: '0:1',
464
- validate: cameraConfigRequiredField,
465
- },
466
- cameraConfigVideoFilter: {
467
- value: 'scale=1280:720',
468
- },
469
- cameraConfigAdditionalCommandLine: {
470
- value: '-tune zerolatency',
471
- },
472
- cameraConfigDebug: {
473
- value: false,
474
- },
475
- cameraConfigSnapshotOutput: {
476
- value: 'disabled',
477
- validate: cameraConfigRequiredField,
478
- },
479
- cameraConfigInterfaceName: {
480
- value: '',
481
- },
482
553
  characteristicProperties: {
483
554
  value: '{}',
484
555
  validate: function (value) {
@@ -488,8 +559,7 @@ if (nrchkbExperimental) {
488
559
 
489
560
  try {
490
561
  JSON.parse(value)
491
- } catch (e) {
492
- console.log(e, value)
562
+ } catch (_) {
493
563
  return false
494
564
  }
495
565
 
@@ -499,9 +569,66 @@ if (nrchkbExperimental) {
499
569
  waitForSetupMsg: {
500
570
  value: false
501
571
  },
572
+ outputMode: {
573
+ value: 'events'
574
+ },
502
575
  useEventCallback: {
503
576
  value: false
504
577
  },
578
+ plugins: {
579
+ value: []
580
+ },
581
+ plugin1: {
582
+ value: '',
583
+ type: 'homekit-plugin-instance',
584
+ required: false,
585
+ validate: function () { return true }
586
+ },
587
+ plugin2: {
588
+ value: '',
589
+ type: 'homekit-plugin-instance',
590
+ required: false,
591
+ validate: function () { return true }
592
+ },
593
+ plugin3: {
594
+ value: '',
595
+ type: 'homekit-plugin-instance',
596
+ required: false,
597
+ validate: function () { return true }
598
+ },
599
+ plugin4: {
600
+ value: '',
601
+ type: 'homekit-plugin-instance',
602
+ required: false,
603
+ validate: function () { return true }
604
+ },
605
+ plugin5: {
606
+ value: '',
607
+ type: 'homekit-plugin-instance',
608
+ required: false,
609
+ validate: function () { return true }
610
+ },
611
+ plugin6: {
612
+ value: '',
613
+ type: 'homekit-plugin-instance',
614
+ required: false,
615
+ validate: function () { return true }
616
+ },
617
+ plugin7: {
618
+ value: '',
619
+ type: 'homekit-plugin-instance',
620
+ required: false,
621
+ validate: function () { return true }
622
+ },
623
+ plugin8: {
624
+ value: '',
625
+ type: 'homekit-plugin-instance',
626
+ required: false,
627
+ validate: function () { return true }
628
+ },
629
+ pluginSetupComplete: {
630
+ value: false
631
+ },
505
632
  outputs: {
506
633
  value: 1,
507
634
  },
@@ -518,10 +645,32 @@ if (nrchkbExperimental) {
518
645
  inputs: 1,
519
646
  outputs: 1,
520
647
  outputLabels: function (index) {
648
+ const outputMode = this.outputMode || 'events'
649
+
650
+ if (outputMode === 'legacy') {
651
+ if (index === 0) {
652
+ return 'onChange'
653
+ }
654
+
655
+ if (index === 1) {
656
+ return 'onSet'
657
+ }
658
+
659
+ if (index === 2) {
660
+ return 'camera snapshot'
661
+ }
662
+
663
+ return ''
664
+ }
665
+
521
666
  if (index === 0) {
522
667
  return 'events'
523
668
  }
524
669
 
670
+ if (index === 1 && this.serviceName === 'Camera') {
671
+ return 'camera snapshot'
672
+ }
673
+
525
674
  return ''
526
675
  },
527
676
  icon: 'homekit.png',
@@ -532,9 +681,42 @@ if (nrchkbExperimental) {
532
681
  labelStyle: function () {
533
682
  return this.name ? 'node_label_italic' : ''
534
683
  },
684
+ onadd: function () {
685
+ applyDefaultNodeDocumentation(this, 'homekit-service2')
686
+ },
535
687
  oneditprepare: function () {
536
688
  let node = this
537
689
  let isParentToggle = $('#node-config-input-isParent')
690
+ const cameraPluginId = 'node-red-contrib-homekit-bridged:homebridge-camera-ffmpeg'
691
+ const prefersReducedMotion = function () {
692
+ return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches
693
+ }
694
+ const showEditorSection = function (section) {
695
+ if (prefersReducedMotion()) {
696
+ section.stop(true, true).show()
697
+ return
698
+ }
699
+
700
+ section.stop(true, true).fadeIn('fast')
701
+ }
702
+ const hideEditorSection = function (section) {
703
+ if (prefersReducedMotion()) {
704
+ section.stop(true, true).hide()
705
+ return
706
+ }
707
+
708
+ section.stop(true, true).fadeOut('fast')
709
+ }
710
+ const scrollEditorSectionIntoView = function (element) {
711
+ if (!element || !element.scrollIntoView) {
712
+ return
713
+ }
714
+
715
+ element.scrollIntoView({
716
+ behavior: prefersReducedMotion() ? 'auto' : 'smooth',
717
+ block: 'start'
718
+ })
719
+ }
538
720
 
539
721
  if (node.isParent === false) {
540
722
  isParentToggle.val('false')
@@ -550,11 +732,11 @@ if (nrchkbExperimental) {
550
732
  isParentToggle
551
733
  .change(function () {
552
734
  if (isParentToggle.val() === 'true') {
553
- isLinkedDiv.fadeOut('fast')
554
- isParentDiv.fadeIn('fast')
735
+ hideEditorSection(isLinkedDiv)
736
+ showEditorSection(isParentDiv)
555
737
  } else {
556
- isParentDiv.fadeOut('fast')
557
- isLinkedDiv.fadeIn('fast')
738
+ hideEditorSection(isParentDiv)
739
+ showEditorSection(isLinkedDiv)
558
740
  selectHostType.val("_ADD_")
559
741
  }
560
742
  })
@@ -595,95 +777,1154 @@ if (nrchkbExperimental) {
595
777
  .change()
596
778
 
597
779
  const selectServiceName = $('#node-input-serviceName')
780
+ let availablePlugins = []
598
781
 
599
- Object.keys(serviceTypes).sort().forEach(function (key) {
600
- const serviceOption = $('<option></option>')
601
-
602
- serviceOption
603
- .val(key)
604
- .text(key)
782
+ const normalizePlugins = function (value) {
783
+ if (Array.isArray(value)) {
784
+ return value
785
+ }
605
786
 
606
- if (serviceTypes[key].hasOwnProperty('nrchkbDisabledText')) {
607
- serviceOption.text(serviceTypes[key].nrchkbDisabledText)
608
- serviceOption.attr('disabled', 'disabled');
787
+ if (typeof value === 'string' && value.trim()) {
788
+ try {
789
+ const parsed = JSON.parse(value)
790
+ return Array.isArray(parsed) ? parsed : []
791
+ } catch (_) {
792
+ return []
793
+ }
609
794
  }
610
795
 
611
- selectServiceName.append(serviceOption)
612
- })
796
+ return []
797
+ }
613
798
 
614
- let cameraConfiguration = $('#camera-configuration')
615
- let adaptiveLightningConfiguration = $('#adaptive-lightning-configuration')
799
+ const initialServiceName = node.serviceName || ''
800
+ const initialPlugins = normalizePlugins(node.plugins)
801
+ let pluginSetupComplete =
802
+ node.pluginSetupComplete === true ||
803
+ (initialServiceName !== '' && initialPlugins.length === 0)
616
804
 
617
- selectServiceName
618
- .find('option')
619
- .filter(function () {
620
- return $(this).val() === node.serviceName
621
- })
622
- .attr('selected', true)
805
+ const pluginOverview = $('#node-input-plugin-overview')
806
+ const pluginSections = $('#node-input-plugin-sections')
807
+ const pluginCount = $('#node-input-plugin-count')
808
+ const pluginSummaryList = $('#node-input-plugin-summary-list')
623
809
 
624
- selectServiceName
625
- .change(function () {
626
- if (this.value === 'CameraControl') {
627
- cameraConfiguration.fadeIn('fast')
628
- node.outputs = 2
629
- } else {
630
- cameraConfiguration.fadeOut('fast')
631
- node.outputs = 1
632
- }
810
+ const getPluginMetadata = function (id) {
811
+ return availablePlugins.find(function (entry) {
812
+ return entry.id === id
633
813
  })
634
- .change()
814
+ }
635
815
 
636
- selectServiceName
637
- .change(function () {
638
- if (this.value === 'Lightbulb') {
639
- adaptiveLightningConfiguration.fadeIn('fast')
640
- } else {
641
- adaptiveLightningConfiguration.fadeOut('fast')
642
- }
643
- })
644
- .change()
816
+ const getPluginDisplayLabel = function (plugin) {
817
+ if (!plugin) {
818
+ return ''
819
+ }
645
820
 
646
- $('#node-input-characteristicProperties').typedInput({
647
- type: 'json',
648
- types: ['json'],
649
- })
821
+ const hasDisplayNameCollision = availablePlugins.filter(function (entry) {
822
+ return entry.displayName === plugin.displayName
823
+ }).length > 1
650
824
 
651
- const selectParentService = $('#node-input-parentService')
825
+ return hasDisplayNameCollision
826
+ ? plugin.displayName + ' (' + plugin.packageName + ')'
827
+ : plugin.displayName
828
+ }
652
829
 
653
- const candidateNodes = [
654
- ...RED.nodes.filterNodes({
655
- type: 'homekit-service',
656
- }),
657
- ...RED.nodes.filterNodes({
658
- type: 'homekit-service2',
659
- })
660
- ]
830
+ const getPluginPrerequisiteServiceNames = function (plugin) {
831
+ return plugin && plugin.prerequisites && Array.isArray(plugin.prerequisites.serviceNames)
832
+ ? plugin.prerequisites.serviceNames
833
+ : []
834
+ }
661
835
 
662
- candidateNodes.forEach(function (n) {
663
- if (!n.name || n.name.length < 1 || !n.isParent) {
664
- return
665
- }
836
+ const isPluginCompatibleWithSelectedService = function (plugin) {
837
+ const serviceNames = getPluginPrerequisiteServiceNames(plugin)
838
+ return serviceNames.length === 0 || serviceNames.includes(selectServiceName.val())
839
+ }
666
840
 
667
- if (n.id === node.id) {
668
- return
841
+ const allowsAnotherPluginInstance = function (plugin, selectedId) {
842
+ if (!plugin || plugin.multipleInstances !== false || !selectedId) {
843
+ return true
669
844
  }
670
845
 
671
- if (inSubflow) {
672
- if (n.z !== node.z) {
673
- return
674
- }
675
- } else {
676
- if (!!RED.nodes.subflow(n.z)) {
677
- return
678
- }
846
+ return readPluginsFromList().filter(function (entry) {
847
+ return entry.id === selectedId
848
+ }).length === 0
849
+ }
850
+
851
+ const renderCompatibilityText = function (plugin) {
852
+ const serviceNames = getPluginPrerequisiteServiceNames(plugin)
853
+
854
+ if (!serviceNames.length) {
855
+ return 'Works with any service'
679
856
  }
680
857
 
681
- let sublabel
682
- let tab = RED.nodes.workspace(n.z)
683
- if (tab) {
684
- sublabel = tab.label || tab.id
685
- } else {
686
- tab = RED.nodes.subflow(n.z)
858
+ return 'Works with: ' + serviceNames.join(', ')
859
+ }
860
+
861
+ const renderMultipleInstancesText = function (plugin) {
862
+ return plugin && plugin.multipleInstances === false
863
+ ? 'Single instance'
864
+ : 'Multiple instances allowed'
865
+ }
866
+
867
+ const getPluginSectionId = function (entry, index) {
868
+ return 'node-input-plugin-section-' + (entry.id || 'plugin')
869
+ .replace(/[^a-zA-Z0-9_-]/g, '-') + '-' + index
870
+ }
871
+
872
+ const renderPluginDetails = function (plugin) {
873
+ const upstream = plugin.upstream || {}
874
+ const capabilities = plugin.capabilities
875
+ ? Object.keys(plugin.capabilities).filter(function (key) {
876
+ return plugin.capabilities[key] === true
877
+ }).join(', ')
878
+ : ''
879
+
880
+ return (
881
+ '<strong>' + $('<div>').text(plugin.displayName).html() + '</strong> ' +
882
+ 'v' + $('<div>').text(plugin.version).html() + ' by ' +
883
+ $('<div>').text(plugin.author).html() + '<br>' +
884
+ $('<div>').text(plugin.description).html() + '<br>' +
885
+ '<small>' + $('<div>').text(renderCompatibilityText(plugin)).html() + '. ' +
886
+ $('<div>').text(renderMultipleInstancesText(plugin)).html() + '.</small><br>' +
887
+ (capabilities ? '<small>Features: ' + $('<div>').text(capabilities).html() + '</small><br>' : '') +
888
+ '<small>Package: ' + $('<div>').text(plugin.packageName).html() + '</small>' +
889
+ (upstream.packageName
890
+ ? '<br><small>Uses upstream ' + $('<div>').text(upstream.packageName).html() +
891
+ (upstream.packageVersion ? ' v' + $('<div>').text(upstream.packageVersion).html() : '') +
892
+ '. Upstream authors are credited for the original plugin, not this NRCHKB integration.</small>'
893
+ : '')
894
+ )
895
+ }
896
+
897
+ const cloneObject = function (value) {
898
+ return $.extend(true, {}, value || {})
899
+ }
900
+
901
+ const getPathParts = function (path) {
902
+ return typeof path === 'string' && path.length
903
+ ? path.split('.').filter(function (part) {
904
+ return part.length > 0
905
+ })
906
+ : []
907
+ }
908
+
909
+ const getConfigValue = function (config, path, defaultValue) {
910
+ const parts = getPathParts(path)
911
+ let current = config
912
+
913
+ for (let index = 0; index < parts.length; index += 1) {
914
+ if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, parts[index])) {
915
+ return defaultValue
916
+ }
917
+
918
+ current = current[parts[index]]
919
+ }
920
+
921
+ return current === undefined ? defaultValue : current
922
+ }
923
+
924
+ const setConfigValue = function (config, path, value) {
925
+ const parts = getPathParts(path)
926
+ let current = config
927
+
928
+ parts.forEach(function (part, index) {
929
+ if (index === parts.length - 1) {
930
+ current[part] = value
931
+ return
932
+ }
933
+
934
+ if (!current[part] || typeof current[part] !== 'object') {
935
+ current[part] = {}
936
+ }
937
+
938
+ current = current[part]
939
+ })
940
+ }
941
+
942
+ const getPluginDefaultConfig = function (plugin) {
943
+ return cloneObject(plugin && plugin.editor && plugin.editor.defaultConfig)
944
+ }
945
+
946
+ const mergePluginConfig = function (plugin, config) {
947
+ return $.extend(true, getPluginDefaultConfig(plugin), config || {})
948
+ }
949
+
950
+ const readPluginConfig = function (section, entry, plugin) {
951
+ const config = mergePluginConfig(plugin, entry.config || {})
952
+
953
+ section.find('[data-plugin-config-path]').each(function () {
954
+ const input = $(this)
955
+ const path = input.attr('data-plugin-config-path')
956
+ const field = input.data('pluginField') || {}
957
+
958
+ if (!path) {
959
+ return
960
+ }
961
+
962
+ if (field.type === 'checkbox') {
963
+ setConfigValue(config, path, input.is(':checked'))
964
+ } else if (field.type === 'number') {
965
+ const value = input.val()
966
+ setConfigValue(config, path, value === '' || value === undefined ? undefined : Number(value))
967
+ } else if (field.type === 'dynamic-select') {
968
+ const value = input.val() || input.data('pluginDynamicValue') || ''
969
+ setConfigValue(config, path, value)
970
+ } else {
971
+ setConfigValue(config, path, input.val())
972
+ }
973
+ })
974
+
975
+ return config
976
+ }
977
+
978
+ const readPluginsFromList = function () {
979
+ const plugins = []
980
+ const sections = pluginSections.find('.nrchkb-plugin-section')
981
+
982
+ sections.each(function () {
983
+ const section = $(this)
984
+ const entry = section.data('pluginEntry') || {}
985
+ const id = entry.id
986
+
987
+ if (!id) {
988
+ return
989
+ }
990
+
991
+ const plugin = getPluginMetadata(id)
992
+
993
+ plugins.push({
994
+ id,
995
+ automatic: entry.automatic === true,
996
+ modified: entry.modified === true,
997
+ config: readPluginConfig(section, entry, plugin || {})
998
+ })
999
+ })
1000
+
1001
+ return plugins
1002
+ }
1003
+
1004
+ const syncPluginsInput = function () {
1005
+ const plugins = readPluginsFromList()
1006
+ node.plugins = plugins
1007
+ $('#node-input-plugins').val(JSON.stringify(plugins))
1008
+ }
1009
+
1010
+ const getCameraPluginEntry = function () {
1011
+ return readPluginsFromList().find(function (entry) {
1012
+ return entry.id === cameraPluginId
1013
+ })
1014
+ }
1015
+
1016
+ const buildCameraPluginEntry = function () {
1017
+ const plugin = getPluginMetadata(cameraPluginId)
1018
+ const existing = getCameraPluginEntry()
1019
+ const defaultConfig = getPluginDefaultConfig(plugin || {})
1020
+
1021
+ // If we don't have an existing entry, merge service node's camera config into default config
1022
+ if (!existing && defaultConfig && defaultConfig.camera && defaultConfig.camera.videoConfig) {
1023
+ if (node.cameraConfigSource) {
1024
+ defaultConfig.camera.videoConfig.source = node.cameraConfigSource
1025
+ }
1026
+ if (node.cameraConfigStillImageSource) {
1027
+ defaultConfig.camera.videoConfig.stillImageSource = node.cameraConfigStillImageSource
1028
+ }
1029
+ if (node.cameraConfigMaxStreams !== undefined) {
1030
+ defaultConfig.camera.videoConfig.maxStreams = node.cameraConfigMaxStreams
1031
+ }
1032
+ if (node.cameraConfigMaxWidth !== undefined) {
1033
+ defaultConfig.camera.videoConfig.maxWidth = node.cameraConfigMaxWidth
1034
+ }
1035
+ if (node.cameraConfigMaxHeight !== undefined) {
1036
+ defaultConfig.camera.videoConfig.maxHeight = node.cameraConfigMaxHeight
1037
+ }
1038
+ if (node.cameraConfigMaxFPS !== undefined) {
1039
+ defaultConfig.camera.videoConfig.maxFPS = node.cameraConfigMaxFPS
1040
+ }
1041
+ if (node.cameraConfigMaxBitrate !== undefined) {
1042
+ defaultConfig.camera.videoConfig.maxBitrate = node.cameraConfigMaxBitrate
1043
+ }
1044
+ if (node.cameraConfigVideoCodec) {
1045
+ defaultConfig.camera.videoConfig.vcodec = node.cameraConfigVideoCodec
1046
+ }
1047
+ if (node.cameraConfigAudio !== undefined) {
1048
+ defaultConfig.camera.videoConfig.audio = node.cameraConfigAudio
1049
+ }
1050
+ if (node.cameraConfigPacketSize !== undefined) {
1051
+ defaultConfig.camera.videoConfig.packetSize = node.cameraConfigPacketSize
1052
+ }
1053
+ if (node.cameraConfigMapVideo) {
1054
+ defaultConfig.camera.videoConfig.mapvideo = node.cameraConfigMapVideo
1055
+ }
1056
+ if (node.cameraConfigMapAudio) {
1057
+ defaultConfig.camera.videoConfig.mapaudio = node.cameraConfigMapAudio
1058
+ }
1059
+ if (node.cameraConfigVideoFilter) {
1060
+ defaultConfig.camera.videoConfig.videoFilter = node.cameraConfigVideoFilter
1061
+ }
1062
+ if (node.cameraConfigAdditionalCommandLine) {
1063
+ defaultConfig.camera.videoConfig.encoderOptions = node.cameraConfigAdditionalCommandLine
1064
+ }
1065
+ if (node.cameraConfigDebug !== undefined) {
1066
+ defaultConfig.camera.videoConfig.debug = node.cameraConfigDebug
1067
+ }
1068
+ }
1069
+
1070
+ return {
1071
+ id: cameraPluginId,
1072
+ automatic: existing ? existing.automatic !== false : true,
1073
+ modified: existing ? existing.modified === true : false,
1074
+ config: existing ? existing.config : defaultConfig
1075
+ }
1076
+ }
1077
+
1078
+ const shouldSeedCameraPluginEntry = function () {
1079
+ return (
1080
+ !pluginSetupComplete &&
1081
+ initialServiceName === '' &&
1082
+ selectServiceName.val() === 'Camera' &&
1083
+ !getCameraPluginEntry()
1084
+ )
1085
+ }
1086
+
1087
+ const ensureCameraPluginEntry = function () {
1088
+ if (!availablePlugins.length) {
1089
+ return
1090
+ }
1091
+
1092
+ if (shouldSeedCameraPluginEntry()) {
1093
+ addPluginEntry(buildCameraPluginEntry())
1094
+ pluginSetupComplete = true
1095
+ }
1096
+ }
1097
+
1098
+ const getInitialPluginEntry = function (entry, index) {
1099
+ const indexedEntry = initialPlugins[index]
1100
+
1101
+ if (indexedEntry && indexedEntry.id === entry.id) {
1102
+ return indexedEntry
1103
+ }
1104
+
1105
+ return initialPlugins.find(function (initialEntry) {
1106
+ return initialEntry.id === entry.id
1107
+ })
1108
+ }
1109
+
1110
+ const createPluginAbout = function (plugin) {
1111
+ return $('<div/>', {class: 'nrchkb-plugin-about'})
1112
+ .html(renderPluginDetails(plugin))
1113
+ }
1114
+
1115
+ const createPluginFieldId = function (sectionId, field) {
1116
+ return sectionId + '-' + (field.path || 'field').replace(/[^a-zA-Z0-9_-]/g, '-')
1117
+ }
1118
+
1119
+ const appendPluginFieldHelp = function (row, input, fieldId, field) {
1120
+ const helpText = field.description || field.placeholder || ''
1121
+
1122
+ if (!helpText) {
1123
+ return
1124
+ }
1125
+
1126
+ const helpId = fieldId + '-help'
1127
+ input.attr('aria-describedby', helpId)
1128
+ $('<div/>', {
1129
+ id: helpId,
1130
+ class: 'nrchkb-plugin-instance-help'
1131
+ }).text(helpText).appendTo(row)
1132
+ }
1133
+
1134
+ const renderPluginField = function (body, section, field, config) {
1135
+ const sectionId = section.attr('id')
1136
+ const fieldId = createPluginFieldId(sectionId, field)
1137
+ let value = getConfigValue(config, field.path, field.default)
1138
+ if ((value === '' || value === undefined) && field.type === 'dynamic-select') {
1139
+ const initialEntry = section.data('pluginInitialEntry') || {}
1140
+ const initialValue = getConfigValue(initialEntry.config || {}, field.path, undefined)
1141
+
1142
+ if (initialValue !== '' && initialValue !== undefined) {
1143
+ value = initialValue
1144
+ }
1145
+ }
1146
+ const row = $('<div/>', {
1147
+ class: 'form-row' + (field.type === 'checkbox'
1148
+ ? ' nrchkb-checkbox-row nrchkb-plugin-instance-check-row'
1149
+ : field.type === 'textarea'
1150
+ ? ' nrchkb-plugin-instance-textarea-row'
1151
+ : '')
1152
+ }).appendTo(body)
1153
+
1154
+ let input
1155
+
1156
+ if (field.type === 'checkbox') {
1157
+ const label = $('<label/>', {
1158
+ class: 'nrchkb-checkbox-label nrchkb-plugin-instance-check',
1159
+ for: fieldId
1160
+ }).appendTo(row)
1161
+ input = $('<input/>', {type: 'checkbox', id: fieldId}).appendTo(label)
1162
+ $('<span/>')
1163
+ .append(field.icon ? $('<i/>', {class: 'fa ' + field.icon}) : $())
1164
+ .append(field.icon ? ' ' : '')
1165
+ .append(document.createTextNode(field.label || field.path))
1166
+ .appendTo(label)
1167
+ input.prop('checked', !!value)
1168
+ } else {
1169
+ const label = $('<label/>', {for: fieldId}).appendTo(row)
1170
+
1171
+ if (field.icon) {
1172
+ $('<i/>', {class: 'fa ' + field.icon}).appendTo(label)
1173
+ label.append(' ')
1174
+ }
1175
+
1176
+ label.append(document.createTextNode(field.label || field.path))
1177
+ }
1178
+
1179
+ if (field.type === 'select' || field.type === 'dynamic-select') {
1180
+ input = $('<select/>', {id: fieldId, class: 'nrchkb-plugin-instance-select'}).appendTo(row)
1181
+ ;(field.options || []).forEach(function (option) {
1182
+ $('<option/>')
1183
+ .val(option.value)
1184
+ .text(option.label)
1185
+ .appendTo(input)
1186
+ })
1187
+ input.val(value === undefined ? '' : value)
1188
+ } else if (field.type === 'textarea') {
1189
+ input = $('<textarea/>', {
1190
+ id: fieldId,
1191
+ class: 'nrchkb-plugin-instance-textarea',
1192
+ rows: 4
1193
+ }).appendTo(row)
1194
+ input.val(value === undefined ? '' : value)
1195
+ } else {
1196
+ input = $('<input/>', {
1197
+ type: field.type === 'number' ? 'number' : field.type === 'password' ? 'password' : 'text',
1198
+ id: fieldId,
1199
+ placeholder: field.placeholder || ''
1200
+ }).appendTo(row)
1201
+ input.val(value === undefined ? '' : value)
1202
+ }
1203
+
1204
+ input
1205
+ .attr('data-plugin-config-path', field.path)
1206
+ .data('pluginField', field)
1207
+
1208
+ if (field.type === 'config-node' && field.configNodeType && RED.editor && RED.editor.prepareConfigNodeSelect) {
1209
+ const property = fieldId.replace(/^node-input-/, '')
1210
+ const node = {}
1211
+ node[property] = value === undefined ? '' : value
1212
+
1213
+ RED.editor.prepareConfigNodeSelect(
1214
+ node,
1215
+ property,
1216
+ field.configNodeType,
1217
+ 'node-input'
1218
+ )
1219
+
1220
+ input = $('#' + fieldId)
1221
+ input
1222
+ .attr('data-plugin-config-path', field.path)
1223
+ .data('pluginField', field)
1224
+ }
1225
+
1226
+ if (field.type === 'dynamic-select') {
1227
+ input.attr('data-plugin-dynamic-select', 'true')
1228
+ input.data('pluginDynamicValue', value === undefined ? '' : value)
1229
+ }
1230
+
1231
+ appendPluginFieldHelp(row, input, fieldId, field)
1232
+ }
1233
+
1234
+ const loadDynamicPluginSelect = function (section, input) {
1235
+ const field = input.data('pluginField') || {}
1236
+ const selectedValue = input.val() || input.data('pluginDynamicValue') || ''
1237
+
1238
+ if (!field.optionsUrl) {
1239
+ return
1240
+ }
1241
+
1242
+ let url = field.optionsUrl
1243
+ if (field.optionsDependsOn) {
1244
+ const dependsInput = section.find('[data-plugin-config-path="' + field.optionsDependsOn + '"]')
1245
+ const dependsValue = dependsInput.val()
1246
+
1247
+ if (!dependsValue) {
1248
+ input.empty()
1249
+ $('<option/>').val('').text('Choose a controller first').appendTo(input)
1250
+ input.val('')
1251
+ syncPluginsInput()
1252
+ return
1253
+ }
1254
+
1255
+ url = url.replace('{' + field.optionsDependsOn + '}', encodeURIComponent(dependsValue))
1256
+ }
1257
+
1258
+ input.empty()
1259
+ $('<option/>').val('').text('Loading...').appendTo(input)
1260
+
1261
+ $.getJSON(url, function (options) {
1262
+ input.empty()
1263
+ $('<option/>').val('').text('Choose...').appendTo(input)
1264
+ ;(options || []).forEach(function (option) {
1265
+ $('<option/>')
1266
+ .val(option.value)
1267
+ .text(option.label)
1268
+ .appendTo(input)
1269
+ })
1270
+ if (selectedValue && input.find('option').filter(function () {
1271
+ return $(this).val() === selectedValue
1272
+ }).length === 0) {
1273
+ $('<option/>')
1274
+ .val(selectedValue)
1275
+ .text('Saved camera (' + selectedValue + ')')
1276
+ .appendTo(input)
1277
+ }
1278
+ input.val(selectedValue)
1279
+ input.data('pluginDynamicValue', input.val() || selectedValue)
1280
+ syncPluginsInput()
1281
+ }).fail(function () {
1282
+ input.empty()
1283
+ $('<option/>').val('').text('Unable to load cameras').appendTo(input)
1284
+ if (selectedValue) {
1285
+ $('<option/>').val(selectedValue).text(selectedValue).appendTo(input)
1286
+ input.val(selectedValue)
1287
+ }
1288
+ input.data('pluginDynamicValue', input.val() || selectedValue)
1289
+ syncPluginsInput()
1290
+ })
1291
+ }
1292
+
1293
+ const renderPluginEditor = function (body, entry, plugin, section) {
1294
+ const editor = plugin.editor || {}
1295
+ const editorSections = Array.isArray(editor.sections) ? editor.sections : []
1296
+ const config = mergePluginConfig(plugin, entry.config || {})
1297
+
1298
+ body.append(createPluginAbout(plugin))
1299
+
1300
+ if (!editorSections.length) {
1301
+ $('<div/>', {class: 'alert alert-info nrchkb-info'})
1302
+ .text('This plugin does not provide editor fields.')
1303
+ .appendTo(body)
1304
+ return
1305
+ }
1306
+
1307
+ editorSections.forEach(function (editorSection) {
1308
+ if (editorSection.title) {
1309
+ $('<div/>', {class: 'nrchkb-plugin-overview-name'})
1310
+ .text(editorSection.title)
1311
+ .appendTo(body)
1312
+ }
1313
+
1314
+ if (editorSection.description) {
1315
+ $('<div/>', {class: 'nrchkb-plugin-empty'})
1316
+ .text(editorSection.description)
1317
+ .appendTo(body)
1318
+ }
1319
+
1320
+ ;(editorSection.fields || []).forEach(function (field) {
1321
+ renderPluginField(body, section, field, config)
1322
+ })
1323
+ })
1324
+
1325
+ section.data('pluginEntry', $.extend(true, {}, entry, {config}))
1326
+ section.find('[data-plugin-dynamic-select="true"]').each(function () {
1327
+ loadDynamicPluginSelect(section, $(this))
1328
+ })
1329
+ }
1330
+
1331
+ const renderPluginSections = function () {
1332
+ pluginOverview.empty()
1333
+ pluginSections.empty()
1334
+
1335
+ const entries = normalizePlugins(node.plugins)
1336
+ const configNodeEntries = readPluginSlotEntries()
1337
+ const allEntries = entries.concat(configNodeEntries)
1338
+ pluginCount.text(allEntries.length)
1339
+
1340
+ if (!allEntries.length) {
1341
+ pluginSummaryList.text('No plugins attached')
1342
+ $('<div/>', {class: 'nrchkb-plugin-empty'})
1343
+ .text('No plugins attached.')
1344
+ .appendTo(pluginOverview)
1345
+ return
1346
+ }
1347
+
1348
+ const summaryNames = allEntries.map(function (entry) {
1349
+ const plugin = getPluginMetadata(entry.id)
1350
+ return plugin ? getPluginDisplayLabel(plugin) : entry.id
1351
+ })
1352
+ pluginSummaryList.text(summaryNames.join(', '))
1353
+
1354
+ entries.forEach(function (entry, index) {
1355
+ const plugin = getPluginMetadata(entry.id)
1356
+ const title = plugin
1357
+ ? getPluginDisplayLabel(plugin) + ' v' + plugin.version
1358
+ : entry.id
1359
+ const sectionId = getPluginSectionId(entry, index)
1360
+
1361
+ renderPluginOverviewItem(title, sectionId)
1362
+
1363
+ const section = $('<details/>', {
1364
+ id: sectionId,
1365
+ class: 'nrchkb-section nrchkb-plugin-section',
1366
+ open: true
1367
+ }).appendTo(pluginSections)
1368
+ section.data('pluginEntry', $.extend(true, {}, entry))
1369
+ section.data('pluginInitialEntry', $.extend(true, {}, getInitialPluginEntry(entry, index) || {}))
1370
+
1371
+ const summary = $('<summary/>').appendTo(section)
1372
+ $('<i/>', {class: 'fa fa-plug'}).appendTo(summary)
1373
+ $('<span/>', {class: 'nrchkb-plugin-title'}).text(title).appendTo(summary)
1374
+
1375
+ const body = $('<div/>', {class: 'nrchkb-section-body'}).appendTo(section)
1376
+ const toolbar = $('<div/>', {class: 'nrchkb-plugin-section-toolbar'}).appendTo(body)
1377
+ $('<button/>', {
1378
+ type: 'button',
1379
+ class: 'red-ui-button red-ui-button-small nrchkb-plugin-remove',
1380
+ 'aria-label': 'Remove plugin ' + title,
1381
+ title: 'Remove plugin'
1382
+ })
1383
+ .append($('<i/>', {class: 'fa fa-remove'}))
1384
+ .appendTo(toolbar)
1385
+
1386
+ renderPluginEditor(body, entry, plugin || {}, section)
1387
+ })
1388
+
1389
+ configNodeEntries.forEach(function (entry, index) {
1390
+ renderConfigNodePluginSection(entry, getPluginMetadata(entry.id), index)
1391
+ })
1392
+ }
1393
+ let pluginRenderTimer
1394
+ const schedulePluginSectionsRefresh = function () {
1395
+ clearTimeout(pluginRenderTimer)
1396
+ pluginRenderTimer = window.setTimeout(renderPluginSections, 250)
1397
+ }
1398
+
1399
+ const pluginSlotNames = [
1400
+ 'plugin1',
1401
+ 'plugin2',
1402
+ 'plugin3',
1403
+ 'plugin4',
1404
+ 'plugin5',
1405
+ 'plugin6',
1406
+ 'plugin7',
1407
+ 'plugin8'
1408
+ ]
1409
+
1410
+ const isOccupiedPluginSlotValue = function (value) {
1411
+ return typeof value === 'string' &&
1412
+ value.trim() !== '' &&
1413
+ value !== '_ADD_' &&
1414
+ value !== '_NONE_'
1415
+ }
1416
+
1417
+ const getPluginSlotValue = function (slot) {
1418
+ const inputValue = $('#node-input-' + slot).val()
1419
+ const savedValue = node[slot]
1420
+
1421
+ if (isOccupiedPluginSlotValue(inputValue)) {
1422
+ return inputValue
1423
+ }
1424
+
1425
+ if (isOccupiedPluginSlotValue(savedValue)) {
1426
+ return savedValue
1427
+ }
1428
+
1429
+ return ''
1430
+ }
1431
+
1432
+ const parsePluginInstanceConfig = function (value) {
1433
+ if (!value || typeof value !== 'string') {
1434
+ return {}
1435
+ }
1436
+
1437
+ try {
1438
+ const parsed = JSON.parse(value)
1439
+ return parsed && typeof parsed === 'object' ? parsed : {}
1440
+ } catch (_) {
1441
+ return {}
1442
+ }
1443
+ }
1444
+
1445
+ const readPluginSlotEntries = function () {
1446
+ return pluginSlotNames.map(function (slot) {
1447
+ const configNodeId = getPluginSlotValue(slot)
1448
+ const configNode = configNodeId ? RED.nodes.node(configNodeId) : undefined
1449
+ const pluginId = configNode && configNode.pluginId
1450
+
1451
+ if (!configNodeId || !pluginId) {
1452
+ return undefined
1453
+ }
1454
+
1455
+ return {
1456
+ id: pluginId,
1457
+ slot,
1458
+ configNodeId,
1459
+ config: parsePluginInstanceConfig(configNode.pluginConfig)
1460
+ }
1461
+ }).filter(function (entry) {
1462
+ return !!entry
1463
+ })
1464
+ }
1465
+
1466
+ const renderPluginOverviewItem = function (title, sectionId, options) {
1467
+ const overviewItem = $('<div/>', {class: 'nrchkb-plugin-overview-item'}).appendTo(pluginOverview)
1468
+ const overviewText = $('<div/>').appendTo(overviewItem)
1469
+ $('<div/>', {class: 'nrchkb-plugin-overview-name'}).text(title).appendTo(overviewText)
1470
+ const overviewActions = $('<div/>', {class: 'nrchkb-plugin-overview-actions'}).appendTo(overviewItem)
1471
+ $('<button/>', {
1472
+ type: 'button',
1473
+ class: 'red-ui-button red-ui-button-small nrchkb-plugin-overview-edit',
1474
+ 'aria-label': 'Edit plugin settings for ' + title,
1475
+ title: 'Edit plugin settings',
1476
+ 'data-target': sectionId,
1477
+ 'data-slot': options && options.slot ? options.slot : '',
1478
+ 'data-config-id': options && options.configNodeId ? options.configNodeId : ''
1479
+ })
1480
+ .append($('<i/>', {class: 'fa fa-pencil'}))
1481
+ .append(' Edit')
1482
+ .appendTo(overviewActions)
1483
+ $('<button/>', {
1484
+ type: 'button',
1485
+ class: 'red-ui-button red-ui-button-small nrchkb-plugin-overview-remove',
1486
+ 'aria-label': 'Remove plugin ' + title,
1487
+ title: 'Remove plugin',
1488
+ 'data-target': sectionId,
1489
+ 'data-slot': options && options.slot ? options.slot : ''
1490
+ })
1491
+ .append($('<i/>', {class: 'fa fa-remove'}))
1492
+ .append(' Remove')
1493
+ .appendTo(overviewActions)
1494
+ }
1495
+
1496
+ const renderConfigNodePluginSection = function (entry, plugin, index) {
1497
+ const title = plugin
1498
+ ? getPluginDisplayLabel(plugin) + ' v' + plugin.version
1499
+ : entry.id
1500
+ const sectionId = 'node-input-plugin-config-section-' + entry.slot + '-' + index
1501
+
1502
+ renderPluginOverviewItem(title, sectionId, entry)
1503
+
1504
+ const section = $('<details/>', {
1505
+ id: sectionId,
1506
+ class: 'nrchkb-section nrchkb-plugin-section',
1507
+ open: true,
1508
+ 'data-plugin-slot': entry.slot
1509
+ }).appendTo(pluginSections)
1510
+
1511
+ const summary = $('<summary/>').appendTo(section)
1512
+ $('<i/>', {class: 'fa fa-plug'}).appendTo(summary)
1513
+ $('<span/>', {class: 'nrchkb-plugin-title'}).text(title).appendTo(summary)
1514
+
1515
+ const body = $('<div/>', {class: 'nrchkb-section-body'}).appendTo(section)
1516
+ const toolbar = $('<div/>', {class: 'nrchkb-plugin-section-toolbar'}).appendTo(body)
1517
+ $('<button/>', {
1518
+ type: 'button',
1519
+ class: 'red-ui-button red-ui-button-small nrchkb-plugin-remove',
1520
+ 'aria-label': 'Remove plugin ' + title,
1521
+ title: 'Remove plugin',
1522
+ 'data-slot': entry.slot
1523
+ })
1524
+ .append($('<i/>', {class: 'fa fa-remove'}))
1525
+ .appendTo(toolbar)
1526
+
1527
+ if (plugin) {
1528
+ body.append(createPluginAbout(plugin))
1529
+ }
1530
+ }
1531
+
1532
+ const getFirstEmptyPluginSlot = function () {
1533
+ return pluginSlotNames.find(function (slot) {
1534
+ return !getPluginSlotValue(slot)
1535
+ })
1536
+ }
1537
+
1538
+ const addConfigNodePlugin = function (plugin) {
1539
+ const slot = getFirstEmptyPluginSlot()
1540
+
1541
+ if (!slot) {
1542
+ RED.notify('This service already has the maximum number of plugin config nodes.', 'warning')
1543
+ return
1544
+ }
1545
+
1546
+ if (!RED.editor || !RED.editor.editConfig) {
1547
+ RED.notify('This plugin does not provide a config node editor.', 'warning')
1548
+ return
1549
+ }
1550
+
1551
+ window.NRCHKBPluginInstanceDraft = {
1552
+ pluginId: plugin.id,
1553
+ pluginConfig: getPluginDefaultConfig(plugin)
1554
+ }
1555
+ RED.editor.editConfig(slot, 'homekit-plugin-instance', '_ADD_', 'node-input', node)
1556
+ pluginSetupComplete = true
1557
+ schedulePluginSectionsRefresh()
1558
+ }
1559
+
1560
+ const addPluginEntry = function (entry) {
1561
+ const plugin = getPluginMetadata(entry.id)
1562
+
1563
+ if (!plugin) {
1564
+ RED.notify('Plugin metadata is not available.', 'warning')
1565
+ return
1566
+ }
1567
+
1568
+ if (!isPluginCompatibleWithSelectedService(plugin)) {
1569
+ RED.notify('Plugin is not compatible with this service.', 'warning')
1570
+ return
1571
+ }
1572
+
1573
+ if (!allowsAnotherPluginInstance(plugin, entry.id)) {
1574
+ RED.notify('This plugin can only be added once to this node.', 'warning')
1575
+ return
1576
+ }
1577
+
1578
+ if (plugin.attachment === 'config-node') {
1579
+ addConfigNodePlugin(plugin)
1580
+ return
1581
+ }
1582
+
1583
+ node.plugins = readPluginsFromList().concat([entry])
1584
+ pluginSetupComplete = true
1585
+ renderPluginSections()
1586
+ syncPluginsInput()
1587
+ }
1588
+
1589
+ const setPluginEntries = function (plugins) {
1590
+ node.plugins = normalizePlugins(plugins)
1591
+ renderPluginSections()
1592
+ syncPluginsInput()
1593
+ }
1594
+
1595
+ const openAddPluginDialog = function () {
1596
+ const dialog = $('<div/>', {class: 'nrchkb-plugin-picker'})
1597
+
1598
+ availablePlugins.forEach(function (plugin) {
1599
+ const compatible = isPluginCompatibleWithSelectedService(plugin)
1600
+ const duplicateAllowed = allowsAnotherPluginInstance(plugin, plugin.id)
1601
+ const disabled = !compatible || !duplicateAllowed
1602
+ const reason = !compatible
1603
+ ? 'Not compatible with ' + (selectServiceName.val() || 'this service')
1604
+ : !duplicateAllowed
1605
+ ? 'Already added'
1606
+ : renderCompatibilityText(plugin)
1607
+ const item = $('<button/>', {
1608
+ class: 'nrchkb-plugin-picker-item',
1609
+ 'aria-label': getPluginDisplayLabel(plugin) + '. ' + reason,
1610
+ disabled,
1611
+ type: 'button'
1612
+ }).appendTo(dialog)
1613
+
1614
+ $('<div/>', {class: 'nrchkb-plugin-picker-title'})
1615
+ .append(
1616
+ $('<span/>').text(getPluginDisplayLabel(plugin)),
1617
+ $('<span/>').text('v' + plugin.version)
1618
+ )
1619
+ .appendTo(item)
1620
+ $('<div/>', {class: 'nrchkb-plugin-picker-description'})
1621
+ .text(plugin.description)
1622
+ .appendTo(item)
1623
+ $('<div/>', {class: 'nrchkb-plugin-picker-meta'})
1624
+ .text(reason + ' · ' + renderMultipleInstancesText(plugin))
1625
+ .appendTo(item)
1626
+
1627
+ if (!disabled) {
1628
+ const addSelectedPlugin = function () {
1629
+ addPluginEntry({
1630
+ id: plugin.id,
1631
+ automatic: false,
1632
+ modified: true,
1633
+ config: getPluginDefaultConfig(plugin)
1634
+ })
1635
+ dialog.dialog('close')
1636
+ }
1637
+
1638
+ item.on('click', addSelectedPlugin)
1639
+ }
1640
+ })
1641
+
1642
+ if (!availablePlugins.length) {
1643
+ $('<div/>', {class: 'nrchkb-plugin-empty'})
1644
+ .text('No plugins are registered.')
1645
+ .appendTo(dialog)
1646
+ }
1647
+
1648
+ dialog.dialog({
1649
+ modal: true,
1650
+ resizable: false,
1651
+ width: Math.min(560, $(window).width() - 80),
1652
+ title: 'Add plugin',
1653
+ close: function () {
1654
+ dialog.dialog('destroy').remove()
1655
+ },
1656
+ buttons: {
1657
+ Cancel: function () {
1658
+ $(this).dialog('close')
1659
+ }
1660
+ }
1661
+ })
1662
+ }
1663
+
1664
+ pluginSections.on('click', '.nrchkb-plugin-remove', function (event) {
1665
+ event.preventDefault()
1666
+ event.stopPropagation()
1667
+ const section = $(this).closest('.nrchkb-plugin-section')
1668
+ const slot = $(this).attr('data-slot') || section.attr('data-plugin-slot')
1669
+
1670
+ if (slot) {
1671
+ $('#node-input-' + slot).val('_ADD_').trigger('change')
1672
+ node[slot] = ''
1673
+ pluginSetupComplete = true
1674
+ renderPluginSections()
1675
+ return
1676
+ }
1677
+
1678
+ section.remove()
1679
+ node.plugins = readPluginsFromList()
1680
+ pluginSetupComplete = true
1681
+ renderPluginSections()
1682
+ syncPluginsInput()
1683
+ })
1684
+
1685
+ pluginSections.on('change keyup', '[data-plugin-config-path]', function (event) {
1686
+ const section = $(this).closest('.nrchkb-plugin-section')
1687
+ const entry = section.data('pluginEntry') || {}
1688
+ const plugin = getPluginMetadata(entry.id)
1689
+ const changedInput = $(event.target)
1690
+ const changedPath = changedInput.attr('data-plugin-config-path') || ''
1691
+ const changedField = changedInput.data('pluginField') || {}
1692
+
1693
+ if (changedField.type === 'dynamic-select') {
1694
+ changedInput.data('pluginDynamicValue', changedInput.val() || '')
1695
+ }
1696
+
1697
+ entry.modified = true
1698
+ entry.config = readPluginConfig(section, entry, plugin || {})
1699
+ section.data('pluginEntry', entry)
1700
+ section.find('[data-plugin-dynamic-select="true"]').each(function () {
1701
+ const dynamicInput = $(this)
1702
+ const dynamicField = dynamicInput.data('pluginField') || {}
1703
+ if (dynamicField.optionsDependsOn === changedPath) {
1704
+ dynamicInput.data(
1705
+ 'pluginDynamicValue',
1706
+ dynamicInput.val() || dynamicInput.data('pluginDynamicValue') || ''
1707
+ )
1708
+ loadDynamicPluginSelect(section, dynamicInput)
1709
+ }
1710
+ })
1711
+
1712
+ syncPluginsInput()
1713
+ })
1714
+
1715
+ pluginOverview.on('click', '.nrchkb-plugin-overview-edit', function (event) {
1716
+ event.preventDefault()
1717
+ event.stopPropagation()
1718
+
1719
+ const slot = $(this).attr('data-slot')
1720
+ if (slot) {
1721
+ const configNodeId = $(this).attr('data-config-id') || getPluginSlotValue(slot)
1722
+ if (RED.editor && RED.editor.editConfig) {
1723
+ RED.editor.editConfig(slot, 'homekit-plugin-instance', configNodeId || '_ADD_', 'node-input', node)
1724
+ schedulePluginSectionsRefresh()
1725
+ }
1726
+ return
1727
+ }
1728
+
1729
+ const targetId = $(this).attr('data-target')
1730
+ const section = targetId ? $('#' + targetId) : $()
1731
+
1732
+ if (!section.length) {
1733
+ return
1734
+ }
1735
+
1736
+ section.prop('open', true)
1737
+
1738
+ scrollEditorSectionIntoView(section[0])
1739
+ })
1740
+
1741
+ pluginOverview.on('click', '.nrchkb-plugin-overview-remove', function (event) {
1742
+ event.preventDefault()
1743
+ event.stopPropagation()
1744
+
1745
+ const slot = $(this).attr('data-slot')
1746
+ if (slot) {
1747
+ $('#node-input-' + slot).val('_ADD_').trigger('change')
1748
+ node[slot] = ''
1749
+ pluginSetupComplete = true
1750
+ renderPluginSections()
1751
+ return
1752
+ }
1753
+
1754
+ const targetId = $(this).attr('data-target')
1755
+ const section = targetId ? $('#' + targetId) : $()
1756
+
1757
+ if (section.length) {
1758
+ section.remove()
1759
+ }
1760
+
1761
+ node.plugins = readPluginsFromList()
1762
+ pluginSetupComplete = true
1763
+ renderPluginSections()
1764
+ syncPluginsInput()
1765
+ })
1766
+
1767
+ $('#node-input-add-plugin').on('click', function (event) {
1768
+ event.preventDefault()
1769
+ event.stopPropagation()
1770
+ openAddPluginDialog()
1771
+ })
1772
+
1773
+ pluginSlotNames.forEach(function (slot) {
1774
+ $('#node-input-' + slot).on('change', function () {
1775
+ node[slot] = getPluginSlotValue(slot)
1776
+ renderPluginSections()
1777
+ })
1778
+ })
1779
+
1780
+ const pluginsDetailsElement = document.getElementById('plugins-configuration')
1781
+ const pluginsSummaryElement = pluginsDetailsElement && pluginsDetailsElement.querySelector('summary')
1782
+ const updatePluginsExpandedState = function () {
1783
+ if (pluginsSummaryElement) {
1784
+ pluginsSummaryElement.setAttribute('aria-expanded', pluginsDetailsElement.open ? 'true' : 'false')
1785
+ }
1786
+ }
1787
+
1788
+ if (pluginsSummaryElement) {
1789
+ pluginsSummaryElement.setAttribute('aria-controls', 'node-input-plugin-overview')
1790
+ updatePluginsExpandedState()
1791
+ }
1792
+ if (pluginsDetailsElement) {
1793
+ pluginsDetailsElement.addEventListener('toggle', updatePluginsExpandedState)
1794
+ }
1795
+
1796
+ setPluginEntries(node.plugins)
1797
+
1798
+ $.getJSON('nrchkb/plugins', function (plugins) {
1799
+ availablePlugins = plugins.slice().sort(function (left, right) {
1800
+ return getPluginDisplayLabel(left).localeCompare(getPluginDisplayLabel(right), undefined, {
1801
+ numeric: true,
1802
+ sensitivity: 'base'
1803
+ })
1804
+ })
1805
+ if (selectServiceName.val() === 'Camera') {
1806
+ ensureCameraPluginEntry()
1807
+ }
1808
+ renderPluginSections()
1809
+ syncPluginsInput()
1810
+ })
1811
+
1812
+ Object.keys(serviceTypes).sort().forEach(function (key) {
1813
+ if (serviceTypes[key].nrchkbHiddenInService2) {
1814
+ return
1815
+ }
1816
+
1817
+ const serviceOption = $('<option></option>')
1818
+
1819
+ serviceOption
1820
+ .val(key)
1821
+ .text(key)
1822
+
1823
+ if (serviceTypes[key].hasOwnProperty('nrchkbDisabledText')) {
1824
+ serviceOption.text(serviceTypes[key].nrchkbDisabledText)
1825
+ serviceOption.attr('disabled', 'disabled');
1826
+ }
1827
+
1828
+ selectServiceName.append(serviceOption)
1829
+ })
1830
+
1831
+ let adaptiveLightningConfiguration = $('#adaptive-lightning-configuration')
1832
+ const selectOutputMode = $('#node-input-outputMode')
1833
+
1834
+ if (!node.outputMode) {
1835
+ selectOutputMode.val('events')
1836
+ }
1837
+
1838
+ const updateOutputs = function () {
1839
+ const outputMode = selectOutputMode.val() || 'events'
1840
+ const serviceName = selectServiceName.val()
1841
+ const isCamera = serviceName === 'Camera'
1842
+
1843
+ if (outputMode === 'legacy') {
1844
+ node.outputs = isCamera ? 3 : 2
1845
+ } else {
1846
+ node.outputs = isCamera ? 2 : 1
1847
+ }
1848
+ }
1849
+
1850
+ selectServiceName
1851
+ .find('option')
1852
+ .filter(function () {
1853
+ return $(this).val() === node.serviceName
1854
+ })
1855
+ .attr('selected', true)
1856
+
1857
+ selectServiceName
1858
+ .change(function () {
1859
+ if (this.value === 'Camera') {
1860
+ ensureCameraPluginEntry()
1861
+ } else {
1862
+ setPluginEntries(readPluginsFromList().filter(function (entry) {
1863
+ return entry.id !== cameraPluginId || entry.automatic === false || entry.modified
1864
+ }))
1865
+ }
1866
+
1867
+ syncPluginsInput()
1868
+ updateOutputs()
1869
+ })
1870
+ .change()
1871
+
1872
+ selectOutputMode.change(updateOutputs).change()
1873
+
1874
+ selectServiceName
1875
+ .change(function () {
1876
+ if (this.value === 'Lightbulb') {
1877
+ adaptiveLightningConfiguration.prop('open', true)
1878
+ showEditorSection(adaptiveLightningConfiguration)
1879
+ } else {
1880
+ hideEditorSection(adaptiveLightningConfiguration)
1881
+ }
1882
+ })
1883
+ .change()
1884
+
1885
+ $('#node-input-characteristicProperties').typedInput({
1886
+ type: 'json',
1887
+ types: ['json'],
1888
+ })
1889
+
1890
+ const selectParentService = $('#node-input-parentService')
1891
+
1892
+ const candidateNodes = [
1893
+ ...RED.nodes.filterNodes({
1894
+ type: 'homekit-service',
1895
+ }),
1896
+ ...RED.nodes.filterNodes({
1897
+ type: 'homekit-service2',
1898
+ })
1899
+ ]
1900
+
1901
+ const parentServiceOptions = []
1902
+
1903
+ candidateNodes.forEach(function (n) {
1904
+ if (!n.name || n.name.length < 1 || !n.isParent) {
1905
+ return
1906
+ }
1907
+
1908
+ if (n.id === node.id) {
1909
+ return
1910
+ }
1911
+
1912
+ if (inSubflow) {
1913
+ if (n.z !== node.z) {
1914
+ return
1915
+ }
1916
+ } else {
1917
+ if (!!RED.nodes.subflow(n.z)) {
1918
+ return
1919
+ }
1920
+ }
1921
+
1922
+ let sublabel
1923
+ let tab = RED.nodes.workspace(n.z)
1924
+ if (tab) {
1925
+ sublabel = tab.label || tab.id
1926
+ } else {
1927
+ tab = RED.nodes.subflow(n.z)
687
1928
  sublabel = 'subflow : ' + tab.name
688
1929
  }
689
1930
 
@@ -691,14 +1932,17 @@ if (nrchkbExperimental) {
691
1932
  const text = n.name + ' (' + sublabel + ')'
692
1933
  const hostType = n.hostType
693
1934
 
694
- selectParentService.append(
695
- $('<option></option>')
1935
+ parentServiceOptions.push({
1936
+ text,
1937
+ element: $('<option></option>')
696
1938
  .val(value)
697
1939
  .text(text)
698
- .attr("hostType", hostType),
699
- )
1940
+ .attr("hostType", hostType)
1941
+ })
700
1942
  })
701
1943
 
1944
+ sortSelectOptionsByLabel(selectParentService, parentServiceOptions)
1945
+
702
1946
  selectParentService
703
1947
  .find('option')
704
1948
  .filter(function () {
@@ -771,8 +2015,30 @@ if (nrchkbExperimental) {
771
2015
  node.serviceName = $(
772
2016
  '#node-input-serviceName option:selected',
773
2017
  ).val()
2018
+ node.outputMode = $('#node-input-outputMode').val() || 'events'
2019
+ try {
2020
+ const plugins = JSON.parse($('#node-input-plugins').val() || '[]')
2021
+ node.plugins = Array.isArray(plugins) ? plugins : []
2022
+ } catch (_) {
2023
+ node.plugins = []
2024
+ }
2025
+ node.plugin1 = $('#node-input-plugin1').val() || ''
2026
+ node.plugin2 = $('#node-input-plugin2').val() || ''
2027
+ node.plugin3 = $('#node-input-plugin3').val() || ''
2028
+ node.plugin4 = $('#node-input-plugin4').val() || ''
2029
+ node.plugin5 = $('#node-input-plugin5').val() || ''
2030
+ node.plugin6 = $('#node-input-plugin6').val() || ''
2031
+ node.plugin7 = $('#node-input-plugin7').val() || ''
2032
+ node.plugin8 = $('#node-input-plugin8').val() || ''
2033
+ node.pluginSetupComplete = true
2034
+ node.outputs =
2035
+ node.outputMode === 'legacy'
2036
+ ? node.serviceName === 'Camera'
2037
+ ? 3
2038
+ : 2
2039
+ : node.serviceName === 'Camera'
2040
+ ? 2
2041
+ : 1
774
2042
  },
775
2043
  })
776
- }
777
2044
  </script>
778
-