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,22 +1,740 @@
1
1
  <style>
2
- .alert {
2
+ .nrchkb-editor {
3
+ --nrchkb-section-border: var(--red-ui-secondary-border-color, #d8d8d8);
4
+ --nrchkb-section-bg: var(--red-ui-secondary-background, #fff);
5
+ --nrchkb-section-header-bg: var(--red-ui-tertiary-background, #f6f6f6);
6
+ --nrchkb-muted-text: var(--red-ui-secondary-text-color, #666);
7
+ --nrchkb-focus-ring: var(--red-ui-focus-color, #1a73e8);
8
+ --nrchkb-accent: var(--red-ui-text-color-link, #ad1625);
9
+ --nrchkb-warning-border: var(--red-ui-message-warning-border-color, #d7a100);
10
+ --nrchkb-warning-bg: var(--red-ui-message-warning-background, var(--red-ui-tertiary-background, #fff7d6));
11
+ --nrchkb-warning-text: var(--red-ui-message-warning-color, var(--red-ui-primary-text-color, #333));
12
+ --nrchkb-info-border: var(--red-ui-message-info-border-color, var(--red-ui-secondary-border-color, #9ab));
13
+ --nrchkb-info-bg: var(--red-ui-message-info-background, var(--red-ui-tertiary-background, #eef6ff));
14
+ --nrchkb-info-text: var(--red-ui-message-info-color, var(--red-ui-primary-text-color, #333));
15
+ --nrchkb-toggle-on: #34c759;
16
+ --nrchkb-toggle-off: color-mix(in srgb, var(--red-ui-secondary-text-color, #666) 30%, var(--red-ui-tertiary-background, #f6f6f6));
17
+ --nrchkb-toggle-border: color-mix(in srgb, var(--red-ui-secondary-text-color, #666) 20%, transparent);
18
+ --nrchkb-toggle-thumb: #fff;
19
+ --nrchkb-toggle-shadow: 0 1px 2px rgba(0, 0, 0, .22), 0 1px 4px rgba(0, 0, 0, .12);
20
+ container-type: inline-size;
21
+ box-sizing: border-box;
22
+ width: 100%;
23
+ min-width: 0;
24
+ max-width: 100%;
25
+ overflow-x: hidden;
26
+ }
27
+
28
+ .nrchkb-editor *,
29
+ .nrchkb-editor *::before,
30
+ .nrchkb-editor *::after {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ .nrchkb-editor .nrchkb-section {
35
+ margin: 0 0 10px;
36
+ border: 1px solid var(--nrchkb-section-border);
37
+ border-radius: 7px;
38
+ background: var(--nrchkb-section-bg);
39
+ background: color-mix(in srgb, var(--red-ui-secondary-background, #fff) 88%, transparent);
40
+ max-width: 100%;
41
+ overflow: hidden;
42
+ }
43
+
44
+ .nrchkb-editor .nrchkb-section > summary {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 8px;
48
+ min-height: 34px;
49
+ min-width: 0;
50
+ padding: 7px 10px;
51
+ list-style: none;
52
+ cursor: pointer;
53
+ font-weight: 600;
54
+ background: var(--nrchkb-section-header-bg);
55
+ background: color-mix(in srgb, var(--red-ui-tertiary-background, #f6f6f6) 82%, transparent);
56
+ user-select: none;
57
+ }
58
+
59
+ .nrchkb-editor .nrchkb-section > summary:focus-visible,
60
+ .nrchkb-editor button:focus-visible,
61
+ .nrchkb-editor [role="button"]:focus-visible,
62
+ .nrchkb-plugin-picker-item:focus-visible {
63
+ outline: 2px solid var(--nrchkb-focus-ring);
64
+ outline-offset: 2px;
65
+ }
66
+
67
+ .nrchkb-editor .nrchkb-section > summary::-webkit-details-marker {
68
+ display: none;
69
+ }
70
+
71
+ .nrchkb-editor .nrchkb-section > summary::before {
72
+ width: 12px;
73
+ color: var(--nrchkb-muted-text);
74
+ content: "\f0da";
75
+ font-family: FontAwesome;
76
+ text-align: center;
77
+ }
78
+
79
+ .nrchkb-editor .nrchkb-section[open] > summary::before {
80
+ content: "\f0d7";
81
+ }
82
+
83
+ .nrchkb-editor .nrchkb-section > summary i {
84
+ flex: 0 0 14px;
85
+ width: 14px;
86
+ color: var(--nrchkb-muted-text);
87
+ text-align: center;
88
+ }
89
+
90
+ .nrchkb-editor .nrchkb-section-body {
91
+ min-width: 0;
92
+ padding: 10px 10px 2px;
93
+ }
94
+
95
+ .nrchkb-editor .nrchkb-nested-panel {
96
+ margin: 4px 0 8px;
97
+ padding: 10px 10px 2px;
98
+ border: 1px solid var(--nrchkb-section-border);
99
+ border-radius: 6px;
100
+ background: var(--nrchkb-section-bg);
101
+ background: color-mix(in srgb, var(--red-ui-secondary-background, #fff) 72%, transparent);
102
+ min-width: 0;
103
+ max-width: 100%;
104
+ }
105
+
106
+ .nrchkb-editor .form-row {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 10px;
110
+ min-width: 0;
111
+ margin-bottom: 8px;
112
+ }
113
+
114
+ .nrchkb-editor .form-row > label:first-child {
115
+ flex: 0 1 150px;
116
+ width: auto;
117
+ min-width: 98px;
118
+ max-width: min(150px, 42%);
119
+ margin: 0;
120
+ line-height: 1.25;
121
+ }
122
+
123
+ .nrchkb-editor .form-row > label:first-child:not(.nrchkb-checkbox-label) i {
124
+ display: inline-block;
125
+ width: 14px;
126
+ color: var(--nrchkb-muted-text);
127
+ text-align: center;
128
+ }
129
+
130
+ .nrchkb-editor .form-row > label.nrchkb-checkbox-label {
131
+ flex: 1 1 auto;
132
+ width: auto;
133
+ }
134
+
135
+ .nrchkb-editor input[type="text"],
136
+ .nrchkb-editor input[type="number"],
137
+ .nrchkb-editor input:not([type]),
138
+ .nrchkb-editor select,
139
+ .nrchkb-editor .red-ui-typedInput-container {
140
+ min-width: 0;
141
+ max-width: 100%;
142
+ }
143
+
144
+ .nrchkb-editor .form-row > input[type="text"],
145
+ .nrchkb-editor .form-row > input[type="number"],
146
+ .nrchkb-editor .form-row > input:not([type]),
147
+ .nrchkb-editor .form-row > select,
148
+ .nrchkb-editor .form-row > .red-ui-typedInput-container {
149
+ flex: 1 1 auto;
150
+ width: auto !important;
151
+ }
152
+
153
+ .nrchkb-editor .form-row > div {
154
+ min-width: 0;
155
+ max-width: 100%;
156
+ }
157
+
158
+ .nrchkb-editor .form-row > div[style*="inline-flex"] {
159
+ flex: 1 1 auto;
160
+ width: auto !important;
161
+ }
162
+
163
+ .nrchkb-editor .form-row > div[style*="inline-flex"] > select {
164
+ min-width: 0;
165
+ }
166
+
167
+ .nrchkb-editor .form-row > .red-ui-typedInput-container input {
168
+ width: 100% !important;
169
+ }
170
+
171
+ .nrchkb-editor .form-row > .red-ui-typedInput-container {
172
+ min-height: 34px;
173
+ }
174
+
175
+ .nrchkb-editor .nrchkb-checkbox-row {
176
+ align-items: flex-start;
177
+ }
178
+
179
+ .nrchkb-editor input[type="checkbox"] {
180
+ position: relative;
181
+ flex: 0 0 auto;
182
+ width: 36px;
183
+ height: 22px;
184
+ margin: 0;
185
+ border: 1px solid var(--nrchkb-toggle-border);
186
+ border-radius: 999px;
187
+ appearance: none;
188
+ -webkit-appearance: none;
189
+ background: var(--nrchkb-toggle-off);
190
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .03);
191
+ cursor: pointer;
192
+ vertical-align: middle;
193
+ transition: background-color .18s ease, border-color .18s ease, box-shadow .18s ease;
194
+ }
195
+
196
+ .nrchkb-editor input[type="checkbox"]::before {
197
+ position: absolute;
198
+ top: 2px;
199
+ left: 2px;
200
+ width: 16px;
201
+ height: 16px;
202
+ border-radius: 50%;
203
+ background: var(--nrchkb-toggle-thumb);
204
+ box-shadow: var(--nrchkb-toggle-shadow);
205
+ content: "";
206
+ transition: transform .18s ease;
207
+ }
208
+
209
+ .nrchkb-editor input[type="checkbox"]:checked {
210
+ border-color: color-mix(in srgb, var(--nrchkb-toggle-on) 82%, #000);
211
+ background: var(--nrchkb-toggle-on);
212
+ }
213
+
214
+ .nrchkb-editor input[type="checkbox"]:checked::before {
215
+ transform: translateX(14px);
216
+ }
217
+
218
+ .nrchkb-editor input[type="checkbox"]:focus-visible {
219
+ outline: 2px solid var(--nrchkb-focus-ring);
220
+ outline-offset: 2px;
221
+ }
222
+
223
+ .nrchkb-editor input[type="checkbox"]:disabled {
224
+ cursor: not-allowed;
225
+ opacity: .55;
226
+ }
227
+
228
+ .nrchkb-editor .nrchkb-checkbox-label {
229
+ position: relative;
230
+ display: inline-flex;
231
+ align-items: center;
232
+ gap: 8px;
233
+ width: auto;
234
+ margin: 0;
235
+ line-height: 1.35;
236
+ cursor: pointer;
237
+ }
238
+
239
+ .nrchkb-editor .nrchkb-checkbox-label input[type="checkbox"] {
240
+ margin-top: 0;
241
+ }
242
+
243
+ .nrchkb-editor .nrchkb-checkbox-label span {
244
+ min-width: 0;
245
+ }
246
+
247
+ .nrchkb-editor .nrchkb-checkbox-label i {
248
+ position: absolute;
249
+ top: 50%;
250
+ left: 6px;
251
+ z-index: 1;
252
+ width: 10px;
253
+ color: color-mix(in srgb, var(--nrchkb-muted-text) 72%, transparent);
254
+ font-size: 9px;
255
+ line-height: 1;
256
+ text-align: center;
257
+ pointer-events: none;
258
+ transform: translateY(-50%);
259
+ transition: transform .18s ease, color .18s ease;
260
+ }
261
+
262
+ .nrchkb-editor .nrchkb-checkbox-label input[type="checkbox"]:checked + span i {
263
+ color: color-mix(in srgb, var(--nrchkb-toggle-on) 72%, #1f6f35);
264
+ transform: translate(14px, -50%);
265
+ }
266
+
267
+ .nrchkb-editor .form-row > label:not(.nrchkb-checkbox-label) i {
268
+ color: var(--nrchkb-muted-text);
269
+ }
270
+
271
+ .nrchkb-editor .nrchkb-warning {
272
+ margin: 0 0 10px;
273
+ }
274
+
275
+ .nrchkb-editor .nrchkb-advertiser-recommendation {
276
+ display: block;
277
+ flex: 1 1 100%;
278
+ margin: -2px 0 8px;
279
+ padding: 7px 9px;
280
+ border: 1px solid var(--nrchkb-section-border);
281
+ border-radius: 6px;
282
+ color: var(--nrchkb-muted-text);
283
+ background: color-mix(in srgb, var(--red-ui-tertiary-background, #f6f6f6) 78%, transparent);
284
+ line-height: 1.35;
285
+ }
286
+
287
+ .nrchkb-editor .nrchkb-advertiser-recommendation strong {
288
+ color: var(--red-ui-primary-text-color, #333);
289
+ }
290
+
291
+ .nrchkb-editor .nrchkb-pairing-card {
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 12px;
295
+ min-width: 0;
296
+ margin: 0 0 8px;
297
+ }
298
+
299
+ .nrchkb-editor .nrchkb-pairing-setup-card,
300
+ .nrchkb-sidebar-pairing .nrchkb-pairing-setup-card {
301
+ flex: 0 0 220px;
302
+ width: 220px;
303
+ min-width: 0;
304
+ height: auto;
305
+ padding: 0;
306
+ border: 0;
307
+ background: transparent;
308
+ box-sizing: border-box;
309
+ }
310
+
311
+ .nrchkb-editor .nrchkb-pairing-qr,
312
+ .nrchkb-sidebar-pairing .nrchkb-pairing-qr {
313
+ display: block;
314
+ width: 100%;
315
+ height: auto;
316
+ }
317
+
318
+ .nrchkb-editor .nrchkb-pairing-details {
319
+ flex: 1 1 auto;
320
+ min-width: 0;
321
+ }
322
+
323
+ .nrchkb-editor .nrchkb-pairing-state {
324
+ margin: 0 0 6px;
325
+ font-weight: 600;
326
+ }
327
+
328
+ .nrchkb-editor .nrchkb-pairing-code,
329
+ .nrchkb-editor .nrchkb-pairing-uri {
330
+ display: block;
331
+ min-width: 0;
332
+ overflow-wrap: anywhere;
333
+ color: var(--nrchkb-muted-text);
334
+ font-family: monospace;
335
+ line-height: 1.35;
336
+ }
337
+
338
+ .nrchkb-editor .nrchkb-pairing-message {
339
+ margin: 0 0 8px;
340
+ color: var(--nrchkb-muted-text);
341
+ line-height: 1.35;
342
+ }
343
+
344
+ .nrchkb-sidebar-pairing {
345
+ height: 100%;
346
+ overflow-y: auto;
347
+ min-width: 0;
348
+ padding: 10px;
349
+ box-sizing: border-box;
350
+ }
351
+
352
+ .nrchkb-sidebar-pairing-status {
353
+ margin: 0 0 10px;
354
+ color: var(--nrchkb-muted-text);
355
+ line-height: 1.35;
356
+ }
357
+
358
+ .nrchkb-sidebar-pairing label.nrchkb-sidebar-setting {
359
+ display: grid !important;
360
+ grid-template-columns: 16px minmax(0, 1fr);
361
+ align-items: center;
362
+ column-gap: 6px;
363
+ margin: 0 0 10px !important;
364
+ line-height: 16px;
365
+ }
366
+
367
+ .nrchkb-sidebar-pairing label.nrchkb-sidebar-setting input {
368
+ width: 16px;
369
+ height: 16px;
370
+ margin: 0 !important;
371
+ }
372
+
373
+ .nrchkb-sidebar-pairing label.nrchkb-sidebar-setting span {
374
+ display: block;
375
+ min-width: 0;
376
+ }
377
+
378
+ .nrchkb-sidebar-pairing-card {
379
+ display: grid;
380
+ grid-template-rows: auto auto auto 1fr;
381
+ margin: 0 0 12px;
382
+ padding: 10px;
383
+ border: 1px solid var(--nrchkb-section-border);
384
+ border-radius: 7px;
385
+ background: var(--nrchkb-section-bg);
386
+ container-type: inline-size;
387
+ min-width: 0;
388
+ }
389
+
390
+ .nrchkb-sidebar-pairing-card h3 {
391
+ margin: 0 0 8px;
392
+ font-size: 14px;
393
+ line-height: 1.35;
394
+ }
395
+
396
+ .nrchkb-sidebar-pairing-list {
397
+ display: grid;
398
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
399
+ gap: 10px;
400
+ }
401
+
402
+ .nrchkb-sidebar-pairing .nrchkb-pairing-setup-card {
403
+ margin: 0 auto 8px;
404
+ width: min(100%, 220px);
405
+ }
406
+
407
+ .nrchkb-sidebar-meta {
408
+ display: grid;
409
+ grid-template-columns: auto minmax(0, 1fr);
410
+ gap: 3px 8px;
411
+ min-height: 66px;
412
+ margin: 0 0 8px;
413
+ color: var(--nrchkb-muted-text);
414
+ font-size: 12px;
415
+ line-height: 1.35;
416
+ }
417
+
418
+ .nrchkb-sidebar-meta dt {
419
+ margin: 0;
420
+ font-weight: 600;
421
+ }
422
+
423
+ .nrchkb-sidebar-meta dd {
424
+ min-width: 0;
425
+ margin: 0;
426
+ overflow-wrap: anywhere;
427
+ }
428
+
429
+ .nrchkb-sidebar-accessories {
430
+ margin: 8px 0 0;
431
+ padding: 8px 0 0;
432
+ border-top: 1px solid var(--nrchkb-section-border);
433
+ }
434
+
435
+ .nrchkb-sidebar-accessories-title {
436
+ margin: 0 0 5px;
437
+ color: var(--nrchkb-muted-text);
438
+ font-size: 12px;
439
+ font-weight: 600;
440
+ line-height: 1.35;
441
+ }
442
+
443
+ .nrchkb-sidebar-accessories ul {
444
+ margin: 0;
445
+ padding: 0;
446
+ color: var(--nrchkb-muted-text);
447
+ line-height: 1.4;
448
+ list-style: none;
449
+ }
450
+
451
+ .nrchkb-sidebar-accessories li {
452
+ margin: 0 0 7px;
453
+ }
454
+
455
+ .nrchkb-sidebar-accessory-name {
456
+ display: block;
457
+ color: var(--red-ui-primary-text-color, #333);
458
+ font-weight: 600;
459
+ }
460
+
461
+ .nrchkb-sidebar-accessory-meta {
462
+ display: block;
463
+ overflow-wrap: anywhere;
464
+ font-size: 12px;
465
+ }
466
+
467
+ .nrchkb-sidebar-pairing-control {
468
+ margin: 0 0 10px;
469
+ }
470
+
471
+ .nrchkb-sidebar-toolbar {
472
+ display: flex;
473
+ justify-content: flex-end;
474
+ gap: 6px;
475
+ padding: 6px;
476
+ border-top: 1px solid var(--nrchkb-section-border);
477
+ }
478
+
479
+ .fa-nrchkb::before {
480
+ content: "";
481
+ display: inline-block;
482
+ width: 1.25em;
483
+ height: 1.25em;
484
+ vertical-align: -20%;
485
+ background-image: url("icons/node-red-contrib-homekit-bridged/nrchkb.png");
486
+ background-size: contain;
487
+ background-position: center;
488
+ background-repeat: no-repeat;
489
+ }
490
+
491
+ .nrchkb-help {
492
+ --nrchkb-help-border: var(--red-ui-secondary-border-color, #d8d8d8);
493
+ --nrchkb-help-bg: var(--red-ui-secondary-background, #fff);
494
+ --nrchkb-help-heading-bg: var(--red-ui-tertiary-background, #f6f6f6);
495
+ --nrchkb-help-muted: var(--red-ui-secondary-text-color, #666);
496
+ }
497
+
498
+ .nrchkb-help .nrchkb-help-section {
499
+ margin: 10px 0;
500
+ border: 1px solid var(--nrchkb-help-border);
501
+ border-radius: 7px;
502
+ background: var(--nrchkb-help-bg);
503
+ overflow: hidden;
504
+ }
505
+
506
+ .nrchkb-help .nrchkb-help-section > h4 {
507
+ margin: 0;
508
+ padding: 7px 10px;
509
+ font-size: 13px;
510
+ line-height: 1.35;
511
+ background: var(--nrchkb-help-heading-bg);
512
+ }
513
+
514
+ .nrchkb-help .nrchkb-help-section > h4 i {
515
+ width: 14px;
516
+ margin-right: 6px;
517
+ color: var(--nrchkb-help-muted);
518
+ text-align: center;
519
+ }
520
+
521
+ .nrchkb-help .nrchkb-help-section-body {
522
+ padding: 8px 10px;
523
+ }
524
+
525
+ .nrchkb-help .nrchkb-help-section-body > :first-child {
526
+ margin-top: 0;
527
+ }
528
+
529
+ .nrchkb-help .nrchkb-help-section-body > :last-child {
530
+ margin-bottom: 0;
531
+ }
532
+
533
+ .nrchkb-help .nrchkb-advertiser-dynamic {
534
+ margin: 8px 0;
535
+ padding: 7px 9px;
536
+ border-left: 3px solid var(--red-ui-primary-background, #8f0000);
537
+ border-radius: 4px;
538
+ background: color-mix(in srgb, var(--red-ui-tertiary-background, #f6f6f6) 78%, transparent);
539
+ }
540
+
541
+ .nrchkb-editor .nrchkb-characteristic-item {
542
+ overflow: hidden;
543
+ white-space: normal;
544
+ }
545
+
546
+ .nrchkb-editor .nrchkb-characteristic-row {
547
+ display: flex;
548
+ align-items: center;
549
+ gap: 10px;
550
+ min-width: 0;
551
+ margin-bottom: 8px;
552
+ }
553
+
554
+ .nrchkb-editor .nrchkb-characteristic-fields {
555
+ margin-top: 8px;
556
+ min-width: 0;
557
+ }
558
+
559
+ .nrchkb-editor .nrchkb-characteristic-label {
560
+ flex: 0 1 120px;
561
+ min-width: 98px;
562
+ max-width: min(120px, 42%);
563
+ padding-left: 10px;
564
+ text-align: left;
565
+ }
566
+
567
+ .nrchkb-editor .nrchkb-characteristic-label i,
568
+ .nrchkb-editor .nrchkb-characteristic-property-label i,
569
+ .nrchkb-editor .properties-accordion h3 i {
570
+ width: 14px;
571
+ color: var(--nrchkb-muted-text);
572
+ text-align: center;
573
+ }
574
+
575
+ .nrchkb-editor .nrchkb-characteristic-row input,
576
+ .nrchkb-editor .nrchkb-characteristic-row select,
577
+ .nrchkb-editor .nrchkb-characteristic-fields input,
578
+ .nrchkb-editor .nrchkb-characteristic-fields select {
579
+ flex: 1 1 0;
580
+ min-width: 0;
581
+ width: auto !important;
582
+ }
583
+
584
+ .nrchkb-editor .nrchkb-characteristic-fields .property-validValueRanges {
585
+ flex: 1 1 0;
586
+ min-width: 0;
587
+ max-width: 100%;
588
+ }
589
+
590
+ .nrchkb-editor #node-input-customCharacteristics-container {
591
+ width: 100%;
592
+ min-width: 0;
593
+ min-height: 150px;
594
+ }
595
+
596
+ .nrchkb-editor ol#node-input-customCharacteristics-container .red-ui-typedInput-container {
597
+ flex: 1;
598
+ }
599
+
600
+ .nrchkb-editor .node-input-customCharacteristics-container-row {
601
+ display: block;
602
+ min-width: 0;
603
+ }
604
+
605
+ .nrchkb-editor .red-ui-editableList,
606
+ .nrchkb-editor .red-ui-editableList-border,
607
+ .nrchkb-editor .red-ui-editableList-container,
608
+ .nrchkb-editor .red-ui-editableList-list,
609
+ .nrchkb-editor .red-ui-editableList-list > li {
610
+ min-width: 0;
611
+ max-width: 100%;
612
+ }
613
+
614
+ .nrchkb-editor .ui-slider .ui-slider-handle {
615
+ background: var(--nrchkb-accent);
616
+ }
617
+
618
+ @container (max-width: 520px) {
619
+ .nrchkb-editor .form-row {
620
+ align-items: stretch;
621
+ flex-direction: column;
622
+ gap: 4px;
623
+ }
624
+
625
+ .nrchkb-editor .form-row > label:first-child {
626
+ flex: 0 0 auto;
627
+ max-width: 100%;
628
+ min-width: 0;
629
+ width: auto;
630
+ }
631
+
632
+ .nrchkb-editor .form-row > input[type="text"],
633
+ .nrchkb-editor .form-row > input[type="number"],
634
+ .nrchkb-editor .form-row > input:not([type]),
635
+ .nrchkb-editor .form-row > select,
636
+ .nrchkb-editor .form-row > .red-ui-typedInput-container,
637
+ .nrchkb-editor .form-row > div[style*="inline-flex"] {
638
+ flex: 0 0 auto;
639
+ width: 100% !important;
640
+ }
641
+
642
+ .nrchkb-editor .nrchkb-checkbox-row {
643
+ display: block;
644
+ }
645
+
646
+ .nrchkb-editor .nrchkb-characteristic-row {
647
+ align-items: stretch;
648
+ flex-direction: column;
649
+ gap: 4px;
650
+ }
651
+
652
+ .nrchkb-editor .nrchkb-characteristic-label {
653
+ flex: 0 0 auto;
654
+ max-width: 100%;
655
+ min-width: 0;
656
+ padding-left: 0;
657
+ }
658
+
659
+ .nrchkb-editor .nrchkb-pairing-card {
660
+ align-items: flex-start;
661
+ flex-direction: column;
662
+ }
663
+ }
664
+
665
+ @media (max-width: 520px) {
666
+ .nrchkb-editor .form-row {
667
+ align-items: stretch;
668
+ flex-direction: column;
669
+ gap: 4px;
670
+ }
671
+
672
+ .nrchkb-editor .form-row > label:first-child {
673
+ flex: 0 0 auto;
674
+ max-width: 100%;
675
+ min-width: 0;
676
+ width: auto;
677
+ }
678
+
679
+ .nrchkb-editor .form-row > input[type="text"],
680
+ .nrchkb-editor .form-row > input[type="number"],
681
+ .nrchkb-editor .form-row > input:not([type]),
682
+ .nrchkb-editor .form-row > select,
683
+ .nrchkb-editor .form-row > .red-ui-typedInput-container,
684
+ .nrchkb-editor .form-row > div[style*="inline-flex"] {
685
+ flex: 0 0 auto;
686
+ width: 100% !important;
687
+ }
688
+
689
+ .nrchkb-editor .nrchkb-checkbox-row {
690
+ display: block;
691
+ }
692
+
693
+ .nrchkb-editor .nrchkb-characteristic-row {
694
+ align-items: stretch;
695
+ flex-direction: column;
696
+ gap: 4px;
697
+ }
698
+
699
+ .nrchkb-editor .nrchkb-characteristic-label {
700
+ flex: 0 0 auto;
701
+ max-width: 100%;
702
+ min-width: 0;
703
+ padding-left: 0;
704
+ }
705
+
706
+ .nrchkb-editor .nrchkb-pairing-card {
707
+ align-items: flex-start;
708
+ flex-direction: column;
709
+ }
710
+ }
711
+
712
+ .nrchkb-editor .alert {
3
713
  position: relative;
4
714
  padding: .75rem 1.25rem;
5
715
  margin-bottom: 1rem;
6
- border: 1px solid transparent;
716
+ border: 1px solid var(--nrchkb-section-border);
7
717
  border-radius: .25rem;
8
- margin-right: 60px;
718
+ margin-right: 0;
719
+ color: var(--red-ui-primary-text-color, #333);
720
+ background: var(--red-ui-tertiary-background, #f6f6f6);
721
+ }
722
+
723
+ .nrchkb-editor .alert-warning {
724
+ color: var(--nrchkb-warning-text);
725
+ background: var(--nrchkb-warning-bg);
726
+ border-color: var(--nrchkb-warning-border);
9
727
  }
10
728
 
11
- .alert-warning {
12
- color: #856404;
13
- background-color: #fff3cd;
14
- border-color: #ffeeba;
729
+ .nrchkb-editor .alert-info {
730
+ color: var(--nrchkb-info-text);
731
+ background: var(--nrchkb-info-bg);
732
+ border-color: var(--nrchkb-info-border);
15
733
  }
16
734
  </style>
17
735
 
18
736
  <script type="text/javascript">
19
- const initExperimental = function () {
737
+ const initNRCHKBConfigNode = function () {
20
738
  //NRCHKB Custom Characteristics
21
739
  $.ajax({
22
740
  url: 'nrchkb/config',
@@ -43,6 +761,9 @@
43
761
  labelStyle: function () {
44
762
  return 'node_label_italic'
45
763
  },
764
+ onadd: function () {
765
+ applyDefaultNodeDocumentation(this, 'nrchkb')
766
+ },
46
767
  oneditsave: function () {
47
768
  const self = this
48
769
  saveCustomCharacteristics(self)
@@ -55,38 +776,45 @@
55
776
  types: ["json"]
56
777
  })*/
57
778
 
58
- $('#node-input-customCharacteristics-container').css('min-height', '150px').css('min-width', '550px').editableList({
779
+ $('#node-input-customCharacteristics-container').editableList({
59
780
  addItem: function (container, i, opt) {
60
781
  $('.properties-accordion').accordion('option', 'active', false)
61
782
 
62
783
  const {name, UUID, ...props} = opt
63
784
 
64
- container.css({
65
- overflow: 'hidden',
66
- whiteSpace: 'nowrap'
67
- })
785
+ container.addClass('nrchkb-characteristic-item')
68
786
 
69
787
  const fragment = document.createDocumentFragment()
70
- const row1 = $('<div/>', {style: 'display:flex; align-items: center;'}).appendTo(fragment)
71
- const row2 = $('<div/>', {style: 'display:flex; margin-top:8px; align-items: center;'}).appendTo(fragment)
72
- const row3 = $('<div/>', {style: 'margin-top:8px; align-items: center;'}).appendTo(fragment)
788
+ const row1 = $('<div/>', {class: 'nrchkb-characteristic-row'}).appendTo(fragment)
789
+ const row2 = $('<div/>', {class: 'nrchkb-characteristic-row'}).appendTo(fragment)
790
+ const row3 = $('<div/>', {class: 'nrchkb-characteristic-fields'}).appendTo(fragment)
791
+ const setIconLabelText = function (target, iconClass, text) {
792
+ target.empty()
793
+ $('<i/>', {class: 'fa ' + iconClass}).appendTo(target)
794
+ target.append(document.createTextNode(' ' + text))
795
+ }
796
+ const createPropertyLabel = function (attrs, iconClass, text) {
797
+ const label = $('<label/>', attrs)
798
+ setIconLabelText(label, iconClass, text)
799
+ return label
800
+ }
73
801
 
74
- $('<div/>', {
75
- style: 'display:inline-block;text-align:left; width:120px; padding-left:10px; box-sizing:border-box;',
802
+ const uuidLabel = $('<div/>', {
803
+ class: 'nrchkb-characteristic-label',
76
804
  required: 'required'
77
805
  })
78
- .text('UUID ')
79
806
  .appendTo(row1)
807
+ setIconLabelText(uuidLabel, 'fa-key', 'UUID')
80
808
  $('<input/>', {class: 'property-uuid', type: 'text'})
81
809
  .val(UUID ? UUID : uuidv4)
82
810
  .appendTo(row1)
83
811
 
84
- $('<div/>', {
85
- style: 'display:inline-block;text-align:left; width:120px; padding-left:10px; box-sizing:border-box;',
812
+ const nameLabel = $('<div/>', {
813
+ class: 'nrchkb-characteristic-label',
86
814
  required: 'required'
87
815
  })
88
- .text('Name ')
89
816
  .appendTo(row2)
817
+ setIconLabelText(nameLabel, 'fa-tag', 'Name')
90
818
  $('<input/>', {class: 'property-name', type: 'text'})
91
819
  .val(name)
92
820
  .appendTo(row2)
@@ -94,15 +822,17 @@
94
822
  const row3_properties_accordion = $('<div/>', {class: 'properties-accordion'})
95
823
  .appendTo(row3)
96
824
 
97
- $('<h3/>').text('Properties').appendTo(row3_properties_accordion)
825
+ $('<h3/>')
826
+ .append($('<i/>', {class: 'fa fa-sliders'}), document.createTextNode(' Properties'))
827
+ .appendTo(row3_properties_accordion)
98
828
 
99
829
  const row3_properties = $('<div/>', {class: 'properties'}).appendTo(row3_properties_accordion)
100
830
 
101
831
  const formatRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
102
- $('<label/>', {
103
- class: 'form-row',
832
+ createPropertyLabel({
833
+ class: 'nrchkb-characteristic-property-label',
104
834
  htmlFor: 'property-format'
105
- }).text('Format *').appendTo(formatRow)
835
+ }, 'fa-code', 'Format *').appendTo(formatRow)
106
836
  const formatInput = $('<select/>', {
107
837
  class: 'property-format',
108
838
  required: 'required'
@@ -123,7 +853,10 @@
123
853
  formatInput.val(props.format)
124
854
 
125
855
  const unitRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
126
- $('<label/>', {class: 'form-row', htmlFor: 'property-unit'}).text('Unit').appendTo(unitRow)
856
+ createPropertyLabel({
857
+ class: 'nrchkb-characteristic-property-label',
858
+ htmlFor: 'property-unit'
859
+ }, 'fa-balance-scale', 'Unit').appendTo(unitRow)
127
860
  const unitSelect = $('<select/>', {class: 'property-unit'}).appendTo(unitRow)
128
861
  $('<option/>').val(undefined).text('Choose...').appendTo(unitSelect)
129
862
  $('<option/>').val('celsius').text('CELSIUS').appendTo(unitSelect)
@@ -134,10 +867,10 @@
134
867
  unitSelect.val(props.unit)
135
868
 
136
869
  const permsRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
137
- $('<label/>', {
138
- class: 'form-row',
870
+ createPropertyLabel({
871
+ class: 'nrchkb-characteristic-property-label',
139
872
  htmlFor: 'property-perms'
140
- }).text('Permissions').appendTo(permsRow)
873
+ }, 'fa-lock', 'Permissions').appendTo(permsRow)
141
874
  const permsSelect = $('<select/>', {
142
875
  class: 'property-perms',
143
876
  multiple: 'multiple'
@@ -152,10 +885,10 @@
152
885
  permsSelect.val(props.perms)
153
886
 
154
887
  const evRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
155
- $('<label/>', {
156
- class: 'form-row',
888
+ createPropertyLabel({
889
+ class: 'nrchkb-characteristic-property-label',
157
890
  htmlFor: 'property-ev'
158
- }).text('Event Notifications').appendTo(evRow)
891
+ }, 'fa-bell-o', 'Event Notifications').appendTo(evRow)
159
892
  $('<input/>', {
160
893
  class: 'property-ev',
161
894
  type: 'checkbox',
@@ -163,80 +896,80 @@
163
896
  }).appendTo(evRow)
164
897
 
165
898
  const descriptionRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
166
- $('<label/>', {
167
- class: 'form-row',
899
+ createPropertyLabel({
900
+ class: 'nrchkb-characteristic-property-label',
168
901
  htmlFor: 'property-description'
169
- }).text('Description').appendTo(descriptionRow)
902
+ }, 'fa-align-left', 'Description').appendTo(descriptionRow)
170
903
  $('<input/>', {
171
904
  class: 'property-description',
172
905
  type: 'text'
173
906
  }).appendTo(descriptionRow).val(props.description)
174
907
 
175
908
  const minValueRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
176
- $('<label/>', {
177
- class: 'form-row',
909
+ createPropertyLabel({
910
+ class: 'nrchkb-characteristic-property-label',
178
911
  htmlFor: 'property-minValue'
179
- }).text('Minimum Value').appendTo(minValueRow)
912
+ }, 'fa-sort-numeric-asc', 'Minimum Value').appendTo(minValueRow)
180
913
  $('<input/>', {
181
914
  class: 'property-minValue',
182
915
  type: 'number'
183
916
  }).appendTo(minValueRow).val(props.minValue)
184
917
 
185
918
  const maxValueRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
186
- $('<label/>', {
187
- class: 'form-row',
919
+ createPropertyLabel({
920
+ class: 'nrchkb-characteristic-property-label',
188
921
  htmlFor: 'property-maxValue'
189
- }).text('Maximum Value').appendTo(maxValueRow)
922
+ }, 'fa-sort-numeric-desc', 'Maximum Value').appendTo(maxValueRow)
190
923
  $('<input/>', {
191
924
  class: 'property-maxValue',
192
925
  type: 'number'
193
926
  }).appendTo(maxValueRow).val(props.maxValue)
194
927
 
195
928
  const minStepRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
196
- $('<label/>', {
197
- class: 'form-row',
929
+ createPropertyLabel({
930
+ class: 'nrchkb-characteristic-property-label',
198
931
  htmlFor: 'property-minStep'
199
- }).text('Minimum Step').appendTo(minStepRow)
932
+ }, 'fa-step-forward', 'Minimum Step').appendTo(minStepRow)
200
933
  $('<input/>', {
201
934
  class: 'property-minStep',
202
935
  type: 'number'
203
936
  }).appendTo(minStepRow).val(props.minStep)
204
937
 
205
938
  const maxLenRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
206
- $('<label/>', {
207
- class: 'form-row',
939
+ createPropertyLabel({
940
+ class: 'nrchkb-characteristic-property-label',
208
941
  htmlFor: 'property-maxLen'
209
- }).text('Maximum Length').appendTo(maxLenRow)
942
+ }, 'fa-text-width', 'Maximum Length').appendTo(maxLenRow)
210
943
  $('<input/>', {
211
944
  class: 'property-maxLen',
212
945
  type: 'number'
213
946
  }).appendTo(maxLenRow).val(props.maxLen)
214
947
 
215
948
  const maxDataLenRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
216
- $('<label/>', {
217
- class: 'form-row',
949
+ createPropertyLabel({
950
+ class: 'nrchkb-characteristic-property-label',
218
951
  htmlFor: 'property-maxDataLen'
219
- }).text('Maximum Data Length').appendTo(maxDataLenRow)
952
+ }, 'fa-database', 'Maximum Data Length').appendTo(maxDataLenRow)
220
953
  $('<input/>', {
221
954
  class: 'property-maxDataLen',
222
955
  type: 'number'
223
956
  }).appendTo(maxDataLenRow).val(props.maxDataLen)
224
957
 
225
958
  const validValuesRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
226
- $('<label/>', {
227
- class: 'form-row',
959
+ createPropertyLabel({
960
+ class: 'nrchkb-characteristic-property-label',
228
961
  htmlFor: 'property-validValues'
229
- }).text('Valid Values').appendTo(validValuesRow)
962
+ }, 'fa-check-square-o', 'Valid Values').appendTo(validValuesRow)
230
963
  $('<input/>', {
231
964
  class: 'property-validValues',
232
965
  type: 'text'
233
966
  }).appendTo(validValuesRow).val(props.validValues)
234
967
 
235
968
  const validValueRangesRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
236
- const validValueRangesLabel = $('<label/>', {
237
- class: 'form-row property-validValueRanges-label',
969
+ const validValueRangesLabel = createPropertyLabel({
970
+ class: 'nrchkb-characteristic-property-label property-validValueRanges-label',
238
971
  htmlFor: 'property-validValueRanges'
239
- }).text('Valid Value Ranges: ').appendTo(validValueRangesRow)
972
+ }, 'fa-sliders', 'Valid Value Ranges').appendTo(validValueRangesRow)
240
973
  const validValueRangesSlider = $('<div/>', {
241
974
  class: 'property-validValueRanges',
242
975
  type: 'text'
@@ -248,16 +981,16 @@
248
981
  max: 500,
249
982
  values: props.validValueRanges,
250
983
  slide: function (event, ui) {
251
- validValueRangesLabel.text('Valid Value Ranges: [' + ui.values[0] + ', ' + ui.values[1] + ']')
984
+ setIconLabelText(validValueRangesLabel, 'fa-sliders', 'Valid Value Ranges: [' + ui.values[0] + ', ' + ui.values[1] + ']')
252
985
  }
253
986
  })
254
- validValueRangesLabel.text('Valid Value Ranges: [' + validValueRangesSlider.slider('values', 0) + ', ' + validValueRangesSlider.slider('values', 1) + ']')
987
+ setIconLabelText(validValueRangesLabel, 'fa-sliders', 'Valid Value Ranges: [' + validValueRangesSlider.slider('values', 0) + ', ' + validValueRangesSlider.slider('values', 1) + ']')
255
988
 
256
989
  const adminOnlyAccessRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
257
- $('<label/>', {
258
- class: 'form-row',
990
+ createPropertyLabel({
991
+ class: 'nrchkb-characteristic-property-label',
259
992
  htmlFor: 'property-adminOnlyAccess'
260
- }).text('Admin Only Access').appendTo(adminOnlyAccessRow)
993
+ }, 'fa-user-secret', 'Admin Only Access').appendTo(adminOnlyAccessRow)
261
994
  const adminOnlyAccessSelect = $('<select/>', {
262
995
  class: 'property-adminOnlyAccess',
263
996
  multiple: 'multiple'
@@ -285,15 +1018,15 @@
285
1018
  }
286
1019
  },
287
1020
  oneditresize: function (size) {
288
- const rows = $('#dialog-form>div:not(.node-input-customCharacteristics-container-row)')
1021
+ const rows = $('#dialog-form .nrchkb-section > summary, #dialog-form .nrchkb-section-body > .form-row:not(.node-input-customCharacteristics-container-row)')
289
1022
  let height = size.height
290
1023
  for (let i = 0; i < rows.length; i++) {
291
1024
  height -= $(rows[i]).outerHeight(true)
292
1025
  }
293
- const editorRow = $('#dialog-form>div.node-input-customCharacteristics-container-row')
1026
+ const editorRow = $('#dialog-form .node-input-customCharacteristics-container-row')
294
1027
  height -= (parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom')))
295
1028
  height += 16
296
- $('#node-input-customCharacteristics-container').editableList('height', height)
1029
+ $('#node-input-customCharacteristics-container').editableList('height', Math.max(150, height))
297
1030
  }
298
1031
  })
299
1032
  }
@@ -304,15 +1037,18 @@
304
1037
  return port !== 1880 && port >= 1 && port <= 65535 && port === port.toString()
305
1038
  }
306
1039
 
1040
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor helper consumed by node templates loaded after nrchkb.html.
307
1041
  const isValueDefined = function (value) {
308
1042
  return 'undefined' === typeof value ? false : null !== value
309
1043
  }
310
1044
 
311
- let serviceTypes
312
- let accessoryCategories
1045
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor data consumed by service editors loaded after nrchkb.html.
1046
+ let serviceTypes = {}
1047
+ let accessoryCategories = {}
1048
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor data consumed by config and service editors loaded after nrchkb.html.
313
1049
  let nrchkbVersion = '0.0.0'
314
- let nrchkbExperimental = false
315
1050
  let nrchkbConfig = {}
1051
+ let nrchkbAdvertiserRecommendation = null
316
1052
 
317
1053
  //HomeKit Service Types
318
1054
  $.getJSON('nrchkb/service/types', function (data) {
@@ -331,24 +1067,111 @@
331
1067
  async: false,
332
1068
  success: function (data) {
333
1069
  nrchkbVersion = data.version
334
- nrchkbExperimental = data.experimental
335
-
336
- if (nrchkbExperimental) {
337
- initExperimental()
338
- }
1070
+ initNRCHKBConfigNode()
339
1071
  },
340
1072
  })
341
1073
 
1074
+ const renderAdvertiserRecommendation = function (recommendation) {
1075
+ if (!recommendation || !recommendation.recommended) {
1076
+ return 'Recommendation unavailable.'
1077
+ }
1078
+
1079
+ const caveats = Array.isArray(recommendation.caveats)
1080
+ ? recommendation.caveats
1081
+ : []
1082
+ const caveatText = caveats.length
1083
+ ? ' ' + caveats.join(' ')
1084
+ : ''
1085
+ const reason = recommendation.reason || ''
1086
+ const reasonText = reason.replace(/[.!?]+$/, '') + '.'
1087
+
1088
+ return (
1089
+ '<strong>' +
1090
+ recommendation.title +
1091
+ '</strong>: ' +
1092
+ reasonText +
1093
+ caveatText
1094
+ )
1095
+ }
1096
+
1097
+ const updateAdvertiserHelpTemplates = function () {
1098
+ const renderedRecommendation = renderAdvertiserRecommendation(
1099
+ nrchkbAdvertiserRecommendation,
1100
+ )
1101
+
1102
+ $('script[data-help-name="homekit-bridge"], script[data-help-name="homekit-standalone"]').each(function () {
1103
+ this.innerHTML = this.innerHTML.replace(
1104
+ /<p class="nrchkb-advertiser-dynamic">[\s\S]*?<\/p>/,
1105
+ '<p class="nrchkb-advertiser-dynamic">' +
1106
+ renderedRecommendation +
1107
+ '</p>',
1108
+ )
1109
+ })
1110
+ }
1111
+
1112
+ const applyAdvertiserRecommendation = function (select) {
1113
+ const selectAdvertiser = $(select)
1114
+
1115
+ if (!selectAdvertiser.length) {
1116
+ return
1117
+ }
1118
+
1119
+ const recommendation = nrchkbAdvertiserRecommendation
1120
+
1121
+ selectAdvertiser.find('option').each(function () {
1122
+ const option = $(this)
1123
+ const originalText = option.data('nrchkbOriginalText') || option.text()
1124
+ option.data('nrchkbOriginalText', originalText)
1125
+ option.text(originalText)
1126
+ })
1127
+
1128
+ selectAdvertiser
1129
+ .closest('.form-row')
1130
+ .next('.nrchkb-advertiser-recommendation')
1131
+ .remove()
1132
+
1133
+ if (!recommendation || !recommendation.recommended) {
1134
+ return
1135
+ }
1136
+
1137
+ const recommendedOption = selectAdvertiser.find(
1138
+ 'option[value="' + recommendation.recommended + '"]',
1139
+ )
1140
+
1141
+ if (recommendedOption.length) {
1142
+ recommendedOption.text(
1143
+ recommendedOption.data('nrchkbOriginalText') + ' (recommended)',
1144
+ )
1145
+ }
1146
+
1147
+ selectAdvertiser
1148
+ .closest('.form-row')
1149
+ .after(
1150
+ '<div class="nrchkb-advertiser-recommendation">' +
1151
+ renderAdvertiserRecommendation(recommendation) +
1152
+ '</div>',
1153
+ )
1154
+ }
1155
+
1156
+ $.getJSON('nrchkb/advertiser/recommendation', function (data) {
1157
+ nrchkbAdvertiserRecommendation = data
1158
+ updateAdvertiserHelpTemplates()
1159
+ applyAdvertiserRecommendation('#node-config-input-advertiser')
1160
+ })
1161
+
1162
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor validator consumed by node templates loaded after nrchkb.html.
342
1163
  const versionValidator = function (value) {
343
1164
  return value ? /^(\d+\.)?(\d+\.)?(\.|\d+)$/.test(value) : true
344
1165
  }
345
1166
 
1167
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor validator consumed by config node templates loaded after nrchkb.html.
346
1168
  const hostNameValidator = function (value) {
347
1169
  return value ? /^[^.]{1,64}$/.test(value) : false
348
1170
  }
349
1171
 
1172
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor validator consumed by service templates loaded after nrchkb.html.
350
1173
  const cameraConfigRequiredField = function (value) {
351
- return 'CameraControl' === this.serviceName ? (value || '').toString().trim() : true
1174
+ return ['CameraControl', 'Camera'].includes(this.serviceName) ? (value || '').toString().trim() : true
352
1175
  }
353
1176
 
354
1177
  const saveCustomCharacteristics = function (self) {
@@ -440,6 +1263,7 @@
440
1263
  return `${a}${b}${c}${d}-${e}${f}${g}${h}`
441
1264
  }
442
1265
 
1266
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor validator consumed by config node templates loaded after nrchkb.html.
443
1267
  const validatePinCode = function (value) {
444
1268
  if (!value) {
445
1269
  return false
@@ -451,23 +1275,712 @@
451
1275
 
452
1276
  return !forbiddenPinCodes.includes(value.replaceAll('-', ''))
453
1277
  }
454
- </script>
455
1278
 
456
- <script data-template-name="nrchkb" type="text/x-red">
457
- <style>
458
- ol#node-input-customCharacteristics-container .red-ui-typedInput-container {
459
- flex:1;
1279
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor helper consumed by node templates loaded after nrchkb.html.
1280
+ const sortSelectOptionsByLabel = function (select, options) {
1281
+ const selectElement = $(select)
1282
+ const sortedOptions = (options || []).sort(function (left, right) {
1283
+ return left.text.localeCompare(right.text, undefined, {
1284
+ numeric: true,
1285
+ sensitivity: 'base'
1286
+ })
1287
+ })
1288
+
1289
+ sortedOptions.forEach(function (option) {
1290
+ selectElement.append(option.element)
1291
+ })
1292
+ }
1293
+
1294
+ const nrchkbLegacyDefaultNodeInfo = {
1295
+ 'homekit-status': [
1296
+ '# HomeKit Status',
1297
+ '',
1298
+ '> [!NOTE]',
1299
+ '> Any input message reads the selected HomeKit service and emits a serialized service description.',
1300
+ ].join('\n'),
1301
+ nrchkb: [
1302
+ '# NRCHKB Custom Characteristics',
1303
+ '',
1304
+ '> [!CAUTION]',
1305
+ '> Custom characteristics should be used only when a standard HAP characteristic cannot represent the device state.',
1306
+ ].join('\n'),
1307
+ }
1308
+
1309
+ const nrchkbDefaultNodeInfo = {
1310
+ 'homekit-service2': [
1311
+ '# HomeKit Service 2',
1312
+ '',
1313
+ '> [!IMPORTANT]',
1314
+ '> Send characteristic updates as `msg.payload` keys using HomeKit characteristic names, for example `{\"On\": true}`.',
1315
+ '',
1316
+ '> [!NOTE]',
1317
+ '> Use the node Help sidebar for the full characteristic, plugin, and migration reference.',
1318
+ ].join('\n'),
1319
+ 'homekit-bridge': [
1320
+ '# HomeKit Bridge',
1321
+ '',
1322
+ '> [!IMPORTANT]',
1323
+ '> Keep the bridge name, PIN, serial number, and model stable after pairing. HomeKit uses them as accessory identity.',
1324
+ '',
1325
+ '> [!WARNING]',
1326
+ '> Leave insecure requests disabled unless you are deliberately testing with a trusted local client.',
1327
+ ].join('\n'),
1328
+ 'homekit-standalone': [
1329
+ '# HomeKit Standalone Accessory',
1330
+ '',
1331
+ '> [!IMPORTANT]',
1332
+ '> Standalone accessories publish independently. Prefer a bridge for most multi-service setups to reduce mDNS load.',
1333
+ '',
1334
+ '> [!WARNING]',
1335
+ '> Leave insecure requests disabled unless you are deliberately testing with a trusted local client.',
1336
+ ].join('\n'),
1337
+ 'homekit-service': [
1338
+ '# HomeKit Service',
1339
+ '',
1340
+ '> [!WARNING]',
1341
+ '> This legacy node is kept for existing flows. Use `homekit-service2` for new work and migrate when practical.',
1342
+ '',
1343
+ '> [!NOTE]',
1344
+ '> The migration button prepares changes in the editor. Click Done and then Deploy to commit them.',
1345
+ ].join('\n'),
1346
+ 'homekit-status': [
1347
+ '# HomeKit Status',
1348
+ '',
1349
+ 'Reads the selected `homekit-service` or `homekit-service2` node and outputs a serialized description of its current HomeKit service shape.',
1350
+ '',
1351
+ '> [!NOTE]',
1352
+ '> Any input message triggers a fresh read. The incoming payload is not used as a command.',
1353
+ '',
1354
+ 'Use this node for diagnostics, dashboards, migration checks, or flows that need to inspect which characteristics and values a service currently exposes.',
1355
+ '',
1356
+ 'The output includes the selected service metadata, characteristics, properties, and current values where the runtime can read them.',
1357
+ ].join('\n'),
1358
+ 'homekit-unifi-controller': [
1359
+ '# UniFi Controller',
1360
+ '',
1361
+ '> [!IMPORTANT]',
1362
+ '> Use a dedicated local UniFi account with the minimum permissions needed to read Protect camera data.',
1363
+ '',
1364
+ '> [!WARNING]',
1365
+ '> Allow self-signed certificates only for controllers you administer and trust on your local network.',
1366
+ ].join('\n'),
1367
+ 'homekit-plugin-instance': [
1368
+ '# HomeKit Plugin Instance',
1369
+ '',
1370
+ '> [!NOTE]',
1371
+ '> This internal config node lets Node-RED track plugin dependencies used by NRCHKB service nodes.',
1372
+ ].join('\n'),
1373
+ nrchkb: [
1374
+ '# NRCHKB Custom Characteristics',
1375
+ '',
1376
+ 'Defines custom HAP characteristics that can be attached to HomeKit services when the standard HAP catalog does not contain the value you need.',
1377
+ '',
1378
+ '> [!CAUTION]',
1379
+ '> Custom characteristics should be used only when a standard HAP characteristic cannot represent the device state.',
1380
+ '',
1381
+ 'Each entry needs a stable name and UUID. Changing either one after HomeKit has seen it can make the characteristic appear as a different field to clients.',
1382
+ '',
1383
+ 'Configure value format, permissions, unit, min/max range, step size, and valid values to match the data your flow sends. The runtime loads these definitions before services are built.',
1384
+ ].join('\n'),
1385
+ }
1386
+
1387
+ const applyDefaultNodeDocumentation = function (node, type) {
1388
+ if ((!node.info || node.info === nrchkbLegacyDefaultNodeInfo[type]) && nrchkbDefaultNodeInfo[type]) {
1389
+ node.info = nrchkbDefaultNodeInfo[type]
1390
+ node.dirty = true
1391
+ return true
460
1392
  }
461
- .ui-slider .ui-slider-handle {
462
- background: #ad1625;
1393
+
1394
+ return false
1395
+ }
1396
+
1397
+ const backfillDefaultNodeDocumentation = function (nodes) {
1398
+ let changed = false
1399
+ const applyToNode = function (node) {
1400
+ if (node && applyDefaultNodeDocumentation(node, node.type)) {
1401
+ changed = true
1402
+ }
463
1403
  }
464
- </style>
465
1404
 
466
- <div class="form-row" style="margin-bottom:0;">
467
- <label><i class="fa fa-list"></i> Custom Characteristics</label>
468
- </div>
1405
+ if (Array.isArray(nodes)) {
1406
+ nodes.forEach(applyToNode)
1407
+ } else if (nodes) {
1408
+ applyToNode(nodes)
1409
+ } else if (RED.nodes) {
1410
+ RED.nodes.eachNode(applyToNode)
1411
+
1412
+ if (typeof RED.nodes.eachConfig === 'function') {
1413
+ RED.nodes.eachConfig(applyToNode)
1414
+ }
1415
+ }
1416
+
1417
+ if (changed && RED.view && typeof RED.view.redraw === 'function') {
1418
+ RED.view.redraw(true)
1419
+ }
1420
+ }
1421
+
1422
+ if (RED.events) {
1423
+ RED.events.on('flows:loaded', function () {
1424
+ setTimeout(backfillDefaultNodeDocumentation, 0)
1425
+ })
1426
+
1427
+ RED.events.on('nodes:add', function (node) {
1428
+ setTimeout(function () {
1429
+ backfillDefaultNodeDocumentation(node)
1430
+ }, 0)
1431
+ })
1432
+ }
1433
+
1434
+ window.NRCHKBPairingQR = (function () {
1435
+ const stateByHost = {}
1436
+ const settingsStorageKey = 'nrchkb.pairingQR.settings'
1437
+ let initialized = false
1438
+ let refreshTimer
1439
+ let sidebarContent
1440
+ let sidebarList
1441
+ let sidebarStatus
1442
+ let settings = {
1443
+ autoShowWhenPairingAvailable: true,
1444
+ }
1445
+ const text = function (key, fallback) {
1446
+ const keys = [
1447
+ 'node-red-contrib-homekit-bridged/' + key,
1448
+ 'node-red-contrib-homekit-bridged:' + key,
1449
+ ]
1450
+ try {
1451
+ for (let i = 0; i < keys.length; i++) {
1452
+ const value = RED._(keys[i])
1453
+ if (value && value !== keys[i] && value !== key) {
1454
+ return value
1455
+ }
1456
+ }
1457
+ } catch (_error) {
1458
+ }
1459
+ return fallback
1460
+ }
1461
+
1462
+ const loadSettings = function () {
1463
+ try {
1464
+ const storedSettings = JSON.parse(localStorage.getItem(settingsStorageKey) || '{}')
1465
+ settings = {
1466
+ ...settings,
1467
+ ...storedSettings,
1468
+ autoShowWhenPairingAvailable: storedSettings.autoShowWhenPairingAvailable !== false,
1469
+ }
1470
+ } catch (_error) {
1471
+ settings.autoShowWhenPairingAvailable = true
1472
+ }
1473
+ }
1474
+
1475
+ const saveSettings = function () {
1476
+ try {
1477
+ localStorage.setItem(settingsStorageKey, JSON.stringify(settings))
1478
+ } catch (_error) {
1479
+ }
1480
+ }
1481
+
1482
+ const resolveHostId = function (node) {
1483
+ if (!node) {
1484
+ return ''
1485
+ }
1486
+
1487
+ if ((node.type === 'homekit-service' || node.type === 'homekit-service2') && node.bridge) {
1488
+ return node.bridge
1489
+ }
1490
+
1491
+ if ((node.type === 'homekit-service' || node.type === 'homekit-service2') && node.accessoryId) {
1492
+ return node.accessoryId
1493
+ }
1494
+
1495
+ if (node.type === 'homekit-status' && node.serviceNodeId) {
1496
+ return resolveHostId(RED.nodes.node(node.serviceNodeId))
1497
+ }
1498
+
1499
+ return ''
1500
+ }
1501
+
1502
+ const fetchHostState = function (hostId) {
1503
+ if (!hostId) {
1504
+ return Promise.resolve(undefined)
1505
+ }
1506
+
1507
+ return new Promise(function (resolve) {
1508
+ $.ajax({
1509
+ dataType: 'json',
1510
+ url: 'nrchkb/bridge/' + encodeURIComponent(hostId) + '/pairing',
1511
+ })
1512
+ .done(function (data) {
1513
+ stateByHost[hostId] = data
1514
+ resolve(data)
1515
+ })
1516
+ .fail(function (xhr) {
1517
+ const data = xhr.responseJSON || {
1518
+ paired: false,
1519
+ published: false,
1520
+ status: xhr.status === 404 ? 'missing' : 'error',
1521
+ }
1522
+ stateByHost[hostId] = data
1523
+ resolve(data)
1524
+ })
1525
+ })
1526
+ }
1527
+
1528
+ const getPairingHostNodes = function () {
1529
+ const hosts = []
1530
+ const hostTypes = {
1531
+ 'homekit-bridge': true,
1532
+ 'homekit-standalone': true,
1533
+ }
1534
+
1535
+ if (typeof RED.nodes.eachConfig === 'function') {
1536
+ RED.nodes.eachConfig(function (node) {
1537
+ if (node && hostTypes[node.type]) {
1538
+ hosts.push(node)
1539
+ }
1540
+ })
1541
+ }
1542
+
1543
+ if (hosts.length === 0) {
1544
+ return [
1545
+ ...RED.nodes.filterNodes({type: 'homekit-bridge'}),
1546
+ ...RED.nodes.filterNodes({type: 'homekit-standalone'}),
1547
+ ]
1548
+ }
1549
+
1550
+ return hosts
1551
+ }
1552
+
1553
+ const getCandidateNodes = function () {
1554
+ return [
1555
+ ...RED.nodes.filterNodes({type: 'homekit-service'}),
1556
+ ...RED.nodes.filterNodes({type: 'homekit-service2'}),
1557
+ ...RED.nodes.filterNodes({type: 'homekit-status'}),
1558
+ ]
1559
+ }
1560
+
1561
+ const getReferencingNodes = function (hostId) {
1562
+ return getCandidateNodes().filter(function (node) {
1563
+ return resolveHostId(node) === hostId
1564
+ })
1565
+ }
1566
+
1567
+ const formatPinCode = function (formattedPinCode) {
1568
+ return formattedPinCode ? formattedPinCode.top + ' ' + formattedPinCode.bottom : ''
1569
+ }
1570
+
1571
+ const createPairingSetupCard = function (bridgeState) {
1572
+ const setupCard = $('<div/>', {class: 'nrchkb-pairing-setup-card'})
1573
+ $('<img/>', {
1574
+ alt: text('qr.imageAlt', 'HomeKit pairing QR code'),
1575
+ class: 'nrchkb-pairing-qr',
1576
+ src: bridgeState.qrCodeDataUrl,
1577
+ }).appendTo(setupCard)
1578
+ return setupCard
1579
+ }
1580
+
1581
+ const isPairingAvailable = function (bridgeState) {
1582
+ return !!bridgeState && bridgeState.published && !bridgeState.paired && !!bridgeState.qrCodeDataUrl
1583
+ }
1584
+
1585
+ const nodeDisplayName = function (node) {
1586
+ return node.name || node.serviceName || node.id || text('qr.unknownAccessory', 'Accessory')
1587
+ }
1588
+
1589
+ const serviceTypeName = function (node) {
1590
+ if (!node) {
1591
+ return ''
1592
+ }
1593
+
1594
+ if (node.type === 'homekit-status' && node.serviceNodeId) {
1595
+ return serviceTypeName(RED.nodes.node(node.serviceNodeId))
1596
+ }
1597
+
1598
+ return node.serviceName || node.service || node.type || ''
1599
+ }
1600
+
1601
+ const hostCategoryName = function (hostNode) {
1602
+ if (!hostNode || hostNode.type !== 'homekit-standalone') {
1603
+ return ''
1604
+ }
469
1605
 
470
- <div class="form-row node-input-customCharacteristics-container-row">
471
- <ol id="node-input-customCharacteristics-container"></ol>
1606
+ return accessoryCategories[hostNode.accessoryCategory] || hostNode.accessoryCategory || ''
1607
+ }
1608
+
1609
+ const hostTypeName = function (hostNode) {
1610
+ return hostNode && hostNode.type === 'homekit-standalone' ? 'Standalone accessory' : 'Bridge'
1611
+ }
1612
+
1613
+ const appendMeta = function (container, items) {
1614
+ const meta = $('<dl/>', {class: 'nrchkb-sidebar-meta'}).appendTo(container)
1615
+ items.forEach(function (item) {
1616
+ if (!item.value) {
1617
+ return
1618
+ }
1619
+ $('<dt/>', {text: item.label}).appendTo(meta)
1620
+ $('<dd/>', {text: item.value}).appendTo(meta)
1621
+ })
1622
+ return meta
1623
+ }
1624
+
1625
+ const renderAccessoryListItem = function (list, node) {
1626
+ const item = $('<li/>').appendTo(list)
1627
+ $('<span/>', {
1628
+ class: 'nrchkb-sidebar-accessory-name',
1629
+ text: nodeDisplayName(node),
1630
+ }).appendTo(item)
1631
+ $('<span/>', {
1632
+ class: 'nrchkb-sidebar-accessory-meta',
1633
+ text: 'id: ' + node.id + ', service: ' + serviceTypeName(node),
1634
+ }).appendTo(item)
1635
+ }
1636
+
1637
+ const renderSidebarCardContent = function (card, hostNode, hostState) {
1638
+ card.empty()
1639
+ $('<h3/>', {
1640
+ text: hostState.bridgeName || hostNode.name || hostNode.bridgeName || hostNode.id,
1641
+ }).appendTo(card)
1642
+ appendMeta(card, [
1643
+ {label: 'Type', value: hostTypeName(hostNode)},
1644
+ {label: 'ID', value: hostNode.id},
1645
+ {label: 'Category', value: hostCategoryName(hostNode)},
1646
+ ])
1647
+ createPairingSetupCard(hostState).appendTo(card)
1648
+
1649
+ const accessories = getReferencingNodes(hostNode.id)
1650
+ const accessorySection = $('<div/>', {class: 'nrchkb-sidebar-accessories'}).appendTo(card)
1651
+ $('<p/>', {
1652
+ class: 'nrchkb-sidebar-accessories-title',
1653
+ text: text('qr.accessories', 'Accessories'),
1654
+ }).appendTo(accessorySection)
1655
+
1656
+ if (accessories.length === 0) {
1657
+ $('<p/>', {
1658
+ class: 'nrchkb-pairing-message',
1659
+ text: text('qr.noAccessories', 'No flow nodes reference this pairing host.'),
1660
+ }).appendTo(accessorySection)
1661
+ } else {
1662
+ const list = $('<ul/>').appendTo(accessorySection)
1663
+ accessories.forEach(function (node) {
1664
+ renderAccessoryListItem(list, node)
1665
+ })
1666
+ }
1667
+ }
1668
+
1669
+ const createSidebarCard = function (hostNode, hostState) {
1670
+ const card = $('<div/>', {
1671
+ class: 'nrchkb-sidebar-pairing-card',
1672
+ 'data-host-id': hostNode.id,
1673
+ role: 'listitem',
1674
+ })
1675
+ renderSidebarCardContent(card, hostNode, hostState)
1676
+ return card
1677
+ }
1678
+
1679
+ const sidebarCardSignature = function (hostNode, hostState) {
1680
+ return JSON.stringify({
1681
+ accessories: getReferencingNodes(hostNode.id).map(function (node) {
1682
+ return {
1683
+ id: node.id,
1684
+ name: nodeDisplayName(node),
1685
+ serviceType: serviceTypeName(node),
1686
+ }
1687
+ }),
1688
+ category: hostCategoryName(hostNode),
1689
+ hostId: hostNode.id,
1690
+ hostName: hostState.bridgeName || hostNode.name || hostNode.bridgeName || hostNode.id,
1691
+ hostType: hostTypeName(hostNode),
1692
+ qrCodeDataUrl: hostState.qrCodeDataUrl,
1693
+ })
1694
+ }
1695
+
1696
+ const findSidebarCard = function (hostId) {
1697
+ return sidebarList.children('.nrchkb-sidebar-pairing-card').filter(function () {
1698
+ return $(this).attr('data-host-id') === hostId
1699
+ })
1700
+ }
1701
+
1702
+ const removeSidebarCard = function (card, onComplete) {
1703
+ if (card.data('nrchkbRemoving')) {
1704
+ return
1705
+ }
1706
+
1707
+ card.data('nrchkbRemoving', true)
1708
+ card.stop(true, true).slideUp(180, function () {
1709
+ card.remove()
1710
+ if (onComplete) {
1711
+ onComplete()
1712
+ }
1713
+ })
1714
+ }
1715
+
1716
+ const updateEmptySidebarState = function () {
1717
+ if (sidebarList.children('.nrchkb-sidebar-pairing-card').length === 0) {
1718
+ sidebarStatus.text(text('qr.noUnpaired', 'Nothing to be paired.'))
1719
+ }
1720
+ }
1721
+
1722
+ const refreshSidebar = function (options) {
1723
+ options = options || {}
1724
+
1725
+ if (!sidebarList || !sidebarStatus) {
1726
+ return Promise.resolve()
1727
+ }
1728
+
1729
+ const pairingHosts = getPairingHostNodes()
1730
+
1731
+ if (pairingHosts.length === 0) {
1732
+ sidebarList.children('.nrchkb-sidebar-pairing-card').each(function () {
1733
+ removeSidebarCard($(this), updateEmptySidebarState)
1734
+ })
1735
+ updateEmptySidebarState()
1736
+ return Promise.resolve()
1737
+ }
1738
+
1739
+ return Promise.all(pairingHosts.map(function (hostNode) {
1740
+ return fetchHostState(hostNode.id).then(function (hostState) {
1741
+ return {hostNode, hostState}
1742
+ })
1743
+ })).then(function (results) {
1744
+ const available = results.filter(function (result) {
1745
+ return isPairingAvailable(result.hostState)
1746
+ })
1747
+ const availableById = {}
1748
+
1749
+ available.forEach(function (result) {
1750
+ availableById[result.hostNode.id] = result
1751
+ })
1752
+
1753
+ sidebarList.children('.nrchkb-sidebar-pairing-card').each(function () {
1754
+ const card = $(this)
1755
+ const hostId = card.attr('data-host-id')
1756
+
1757
+ if (!availableById[hostId]) {
1758
+ removeSidebarCard(card, updateEmptySidebarState)
1759
+ }
1760
+ })
1761
+
1762
+ if (available.length === 0) {
1763
+ updateEmptySidebarState()
1764
+ } else {
1765
+ sidebarStatus.text('')
1766
+ available.forEach(function (result) {
1767
+ const signature = sidebarCardSignature(result.hostNode, result.hostState)
1768
+ const card = findSidebarCard(result.hostNode.id)
1769
+
1770
+ if (card.length > 0) {
1771
+ if (card.attr('data-signature') !== signature) {
1772
+ renderSidebarCardContent(card, result.hostNode, result.hostState)
1773
+ card.attr('data-signature', signature)
1774
+ }
1775
+ return
1776
+ }
1777
+
1778
+ const newCard = createSidebarCard(result.hostNode, result.hostState)
1779
+ .attr('data-signature', signature)
1780
+ .hide()
1781
+ sidebarList.append(newCard)
1782
+ newCard.slideDown(180)
1783
+ })
1784
+ }
1785
+
1786
+ if (options.show && available.length > 0 && settings.autoShowWhenPairingAvailable && RED.sidebar) {
1787
+ RED.sidebar.show('nrchkb')
1788
+ }
1789
+ })
1790
+ }
1791
+
1792
+ const registerSidebar = function () {
1793
+ sidebarContent = $('<div/>', {class: 'nrchkb-editor nrchkb-sidebar-pairing'})
1794
+ const section = $('<details/>', {class: 'nrchkb-section', open: true}).appendTo(sidebarContent)
1795
+ $('<summary/>')
1796
+ .append($('<i/>', {class: 'fa fa-qrcode'}))
1797
+ .append(' ')
1798
+ .append($('<span/>', {text: text('qr.sectionTitle', 'Pairing QR Code')}))
1799
+ .appendTo(section)
1800
+ const sectionBody = $('<div/>', {class: 'nrchkb-section-body'}).appendTo(section)
1801
+ const autoShowLabel = $('<label/>', {
1802
+ class: 'nrchkb-sidebar-setting nrchkb-sidebar-pairing-control',
1803
+ }).appendTo(sectionBody)
1804
+ $('<input/>', {
1805
+ checked: settings.autoShowWhenPairingAvailable,
1806
+ type: 'checkbox',
1807
+ })
1808
+ .on('change', function () {
1809
+ settings.autoShowWhenPairingAvailable = this.checked
1810
+ saveSettings()
1811
+ })
1812
+ .appendTo(autoShowLabel)
1813
+ $('<span/>', {
1814
+ text: text('qr.autoShowWhenPairingAvailable', 'Auto show if something needs pairing'),
1815
+ }).appendTo(autoShowLabel)
1816
+ sidebarStatus = $('<p/>', {
1817
+ class: 'nrchkb-sidebar-pairing-status',
1818
+ 'aria-live': 'polite',
1819
+ }).appendTo(sectionBody)
1820
+ sidebarList = $('<div/>', {
1821
+ class: 'nrchkb-sidebar-pairing-list',
1822
+ role: 'list',
1823
+ }).appendTo(sectionBody)
1824
+
1825
+ const toolbar = $('<div/>', {class: 'nrchkb-sidebar-toolbar'})
1826
+ $('<button/>', {
1827
+ class: 'red-ui-button red-ui-button-small',
1828
+ 'aria-label': text('qr.refresh', 'Refresh'),
1829
+ title: text('qr.refresh', 'Refresh'),
1830
+ type: 'button',
1831
+ })
1832
+ .append($('<i/>', {class: 'fa fa-refresh'}))
1833
+ .on('click', function () {
1834
+ refreshSidebar()
1835
+ })
1836
+ .appendTo(toolbar)
1837
+
1838
+ RED.sidebar.addTab({
1839
+ action: 'nrchkb:show-pairing-tab',
1840
+ content: sidebarContent,
1841
+ enableOnEdit: true,
1842
+ iconClass: 'fa fa-nrchkb',
1843
+ id: 'nrchkb',
1844
+ label: text('qr.sidebarTitle', 'NRCHKB'),
1845
+ name: text('qr.sidebarTitle', 'NRCHKB'),
1846
+ pinned: true,
1847
+ toolbar,
1848
+ })
1849
+
1850
+ RED.actions.add('nrchkb:show-pairing-tab', function () {
1851
+ RED.sidebar.show('nrchkb')
1852
+ refreshSidebar()
1853
+ })
1854
+ }
1855
+
1856
+ const renderHostEditor = function (hostId, selector) {
1857
+ const container = $(selector)
1858
+ container.empty().append($('<p/>', {
1859
+ class: 'nrchkb-pairing-message',
1860
+ text: text('qr.loading', 'Loading pairing state...'),
1861
+ }))
1862
+
1863
+ fetchHostState(hostId).then(function (hostState) {
1864
+ container.empty()
1865
+
1866
+ if (!hostState || hostState.status === 'missing' || !hostState.published) {
1867
+ container.append($('<p/>', {
1868
+ class: 'nrchkb-pairing-message',
1869
+ text: text('qr.deployFirst', 'Deploy this bridge or accessory to generate a pairing QR code.'),
1870
+ }))
1871
+ return
1872
+ }
1873
+
1874
+ if (hostState.paired) {
1875
+ container.append($('<p/>', {
1876
+ class: 'nrchkb-pairing-message',
1877
+ text: text('qr.alreadyPaired', 'This bridge or accessory is already paired.'),
1878
+ }))
1879
+ return
1880
+ }
1881
+
1882
+ const card = $('<div/>', {class: 'nrchkb-pairing-card'})
1883
+ createPairingSetupCard(hostState).appendTo(card)
1884
+ const details = $('<div/>', {class: 'nrchkb-pairing-details'}).appendTo(card)
1885
+ $('<p/>', {
1886
+ class: 'nrchkb-pairing-state',
1887
+ text: text('qr.ready', 'Ready to pair'),
1888
+ }).appendTo(details)
1889
+ $('<span/>', {
1890
+ class: 'nrchkb-pairing-code',
1891
+ text: formatPinCode(hostState.formattedPinCode),
1892
+ }).appendTo(details)
1893
+ $('<span/>', {
1894
+ class: 'nrchkb-pairing-uri',
1895
+ text: hostState.setupUri,
1896
+ }).appendTo(details)
1897
+ container.append(card)
1898
+ })
1899
+ }
1900
+
1901
+ const init = function () {
1902
+ if (initialized) {
1903
+ return
1904
+ }
1905
+
1906
+ initialized = true
1907
+
1908
+ loadSettings()
1909
+ registerSidebar()
1910
+
1911
+ if (RED.events) {
1912
+ RED.events.on('deploy', function () {
1913
+ setTimeout(function () {
1914
+ refreshSidebar({show: true})
1915
+ }, 1500)
1916
+ })
1917
+
1918
+ ;['flows:loaded', 'nodes:add', 'nodes:change', 'nodes:remove'].forEach(function (eventName) {
1919
+ RED.events.on(eventName, function () {
1920
+ setTimeout(refreshSidebar, 250)
1921
+ })
1922
+ })
1923
+ }
1924
+
1925
+ refreshTimer = setInterval(refreshSidebar, 10000)
1926
+ window.addEventListener('beforeunload', function () {
1927
+ clearInterval(refreshTimer)
1928
+ })
1929
+ setTimeout(function () {
1930
+ refreshSidebar({show: true})
1931
+ }, 1000)
1932
+ }
1933
+
1934
+ return {
1935
+ init,
1936
+ refreshSidebar,
1937
+ renderBridgeEditor: renderHostEditor,
1938
+ renderHostEditor,
1939
+ }
1940
+ })()
1941
+ RED.NRCHKBPairingQR = window.NRCHKBPairingQR
1942
+
1943
+ setTimeout(function () {
1944
+ if (RED.NRCHKBPairingQR) {
1945
+ RED.NRCHKBPairingQR.init()
1946
+ }
1947
+ }, 1000)
1948
+ </script>
1949
+
1950
+ <script data-template-name="nrchkb" type="text/x-red">
1951
+ <div class="nrchkb-editor">
1952
+ <details class="nrchkb-section" open>
1953
+ <summary><i class="fa fa-list-alt"></i> Custom Characteristics</summary>
1954
+ <div class="nrchkb-section-body">
1955
+ <div class="form-row node-input-customCharacteristics-container-row">
1956
+ <ol id="node-input-customCharacteristics-container"></ol>
1957
+ </div>
1958
+ </div>
1959
+ </details>
472
1960
  </div>
473
- </script>
1961
+ </script>
1962
+
1963
+ <script data-help-name="nrchkb" type="text/markdown">
1964
+ # NRCHKB Custom Characteristics
1965
+
1966
+ Defines custom HAP characteristics that can be attached to HomeKit services when the standard HAP catalog does not contain the value you need.
1967
+
1968
+ > [!CAUTION]
1969
+ > Prefer standard HAP characteristics whenever possible. Custom characteristics may not be shown or interpreted consistently by every HomeKit client.
1970
+
1971
+ ## Characteristic Identity
1972
+
1973
+ - **Name**: Display name used by NRCHKB when selecting or reporting the characteristic.
1974
+ - **UUID**: Stable HAP UUID for the custom characteristic. Keep it unchanged once HomeKit clients have seen it.
1975
+
1976
+ ## Value Shape
1977
+
1978
+ - **Format**: HAP value format such as boolean, integer, float, string, or data.
1979
+ - **Unit**: Optional HomeKit unit metadata for numeric values.
1980
+ - **Minimum / Maximum / Step**: Numeric constraints exposed to HomeKit clients.
1981
+ - **Valid Values**: Optional list of allowed values for enumerated characteristics.
1982
+
1983
+ ## Permissions
1984
+
1985
+ Use read, write, and notify permissions to describe how HomeKit can interact with the value. Runtime flows must still send values matching the configured format and constraints.
1986
+ </script>