node-red-contrib-homekit-bridged 2.0.0-dev.5 → 2.0.0-dev.7

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