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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/build/lib/HAPHostNode.js +183 -141
  2. package/build/lib/HAPServiceNode.js +199 -172
  3. package/build/lib/HAPServiceNode2.js +207 -172
  4. package/build/lib/NRCHKBError.js +23 -2
  5. package/build/lib/PairingQRCode.js +62 -0
  6. package/build/lib/Storage.js +152 -90
  7. package/build/lib/api.js +654 -288
  8. package/build/lib/camera/CameraControl.js +119 -84
  9. package/build/lib/camera/CameraDelegate.js +481 -404
  10. package/build/lib/camera/MP4StreamingServer.js +148 -139
  11. package/build/lib/hap/HAPCharacteristic.js +25 -4
  12. package/build/lib/hap/HAPService.js +25 -4
  13. package/build/lib/hap/eve-app/EveCharacteristics.js +124 -81
  14. package/build/lib/hap/eve-app/EveServices.js +50 -17
  15. package/build/lib/hap/hap-nodejs.js +32 -0
  16. package/build/lib/migration/HomeKitService2Migration.js +34 -0
  17. package/build/lib/migration/NodeMigration.js +75 -0
  18. package/build/lib/types/AccessoryInformationType.js +15 -1
  19. package/build/lib/types/CameraConfigType.js +15 -1
  20. package/build/lib/types/CustomCharacteristicType.js +15 -1
  21. package/build/lib/types/HAPHostConfigType.js +15 -1
  22. package/build/lib/types/HAPHostNodeType.js +15 -1
  23. package/build/lib/types/HAPService2ConfigType.js +15 -1
  24. package/build/lib/types/HAPService2NodeType.js +15 -1
  25. package/build/lib/types/HAPServiceConfigType.js +15 -1
  26. package/build/lib/types/HAPServiceNodeType.js +15 -1
  27. package/build/lib/types/HAPStatusConfigType.js +15 -1
  28. package/build/lib/types/HAPStatusNodeType.js +15 -1
  29. package/build/lib/types/HostType.js +28 -7
  30. package/build/lib/types/NodeType.js +15 -1
  31. package/build/lib/types/PublishTimersType.js +15 -1
  32. package/build/lib/types/UniFiControllerConfigType.js +16 -0
  33. package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.js +28 -7
  34. package/build/lib/types/hap-nodejs/HapCategories.js +64 -43
  35. package/build/lib/types/storage/SerializedHostType.js +15 -1
  36. package/build/lib/types/storage/StorageType.js +34 -10
  37. package/build/lib/unifi/ProtectDiscovery.js +80 -0
  38. package/build/lib/utils/AccessoryUtils.js +152 -110
  39. package/build/lib/utils/BridgeUtils.js +82 -39
  40. package/build/lib/utils/CharacteristicUtils.js +5 -49
  41. package/build/lib/utils/CharacteristicUtils2.js +5 -49
  42. package/build/lib/utils/CharacteristicUtilsBase.js +81 -0
  43. package/build/lib/utils/NodeStatusUtils.js +89 -40
  44. package/build/lib/utils/ServiceUtils.js +433 -375
  45. package/build/lib/utils/ServiceUtils2.js +519 -309
  46. package/build/lib/utils/index.js +10 -11
  47. package/build/nodes/bridge.html +206 -168
  48. package/build/nodes/bridge.js +27 -9
  49. package/build/nodes/locales/en-US/node-red-contrib-homekit-bridged.json +22 -0
  50. package/build/nodes/nrchkb.html +1753 -117
  51. package/build/nodes/nrchkb.js +66 -88
  52. package/build/nodes/plugin-instance.html +509 -0
  53. package/build/nodes/plugin-instance.js +46 -0
  54. package/build/nodes/service.html +544 -307
  55. package/build/nodes/service.js +5 -6
  56. package/build/nodes/service2.html +1721 -455
  57. package/build/nodes/service2.js +5 -8
  58. package/build/nodes/standalone.html +208 -176
  59. package/build/nodes/standalone.js +27 -9
  60. package/build/nodes/status.html +51 -18
  61. package/build/nodes/status.js +47 -40
  62. package/build/nodes/unifi-controller.html +92 -0
  63. package/build/nodes/unifi-controller.js +20 -0
  64. package/build/plugins/embedded/homebridge-camera-ffmpeg/index.js +479 -0
  65. package/build/plugins/embedded/homebridge-unifi-protect/index.js +521 -0
  66. package/build/plugins/embedded/index.js +58 -0
  67. package/build/plugins/nrchkb-homekit-plugins.js +17 -0
  68. package/build/plugins/registry/index.js +203 -0
  69. package/build/plugins/registry/types.js +16 -0
  70. package/build/scripts/migrate-homekit-service-flows.js +47 -0
  71. package/examples/demo/01 - ALL Demos single import.json +1885 -1885
  72. package/examples/demo/02 - Air Purifier.json +279 -279
  73. package/examples/demo/03 - Air Quality sensor with Battery.json +254 -254
  74. package/examples/demo/04 - Dimmable Bulb.json +172 -172
  75. package/examples/demo/05 - Color Bulb (HSV).json +195 -195
  76. package/examples/demo/06 - Fan (simple, 3 speeds).json +240 -240
  77. package/examples/demo/07 - Fan (with speed, oscillate, rotation direction).json +175 -175
  78. package/examples/demo/08 - CO2 detector.json +224 -224
  79. package/examples/demo/09 - CO (carbon monoxide) example.json +255 -255
  80. package/examples/demo/10 - Door window contact sensor.json +234 -234
  81. package/examples/demos (advanced)/01 - Television with inputs and speaker.json +541 -541
  82. package/examples/switch/01 - Plain Switch.json +178 -178
  83. package/package.json +95 -84
  84. package/build/lib/HAPHostNode.d.ts +0 -1
  85. package/build/lib/HAPServiceNode.d.ts +0 -1
  86. package/build/lib/HAPServiceNode2.d.ts +0 -1
  87. package/build/lib/NRCHKBError.d.ts +0 -3
  88. package/build/lib/Storage.d.ts +0 -30
  89. package/build/lib/api.d.ts +0 -1
  90. package/build/lib/camera/CameraControl.d.ts +0 -3
  91. package/build/lib/camera/CameraDelegate.d.ts +0 -38
  92. package/build/lib/camera/MP4StreamingServer.d.ts +0 -26
  93. package/build/lib/hap/HAPCharacteristic.d.ts +0 -9
  94. package/build/lib/hap/HAPService.d.ts +0 -6
  95. package/build/lib/hap/eve-app/EveCharacteristics.d.ts +0 -20
  96. package/build/lib/hap/eve-app/EveServices.d.ts +0 -5
  97. package/build/lib/types/AccessoryInformationType.d.ts +0 -11
  98. package/build/lib/types/CameraConfigType.d.ts +0 -24
  99. package/build/lib/types/CustomCharacteristicType.d.ts +0 -6
  100. package/build/lib/types/HAPHostConfigType.d.ts +0 -22
  101. package/build/lib/types/HAPHostNodeType.d.ts +0 -14
  102. package/build/lib/types/HAPService2ConfigType.d.ts +0 -6
  103. package/build/lib/types/HAPService2NodeType.d.ts +0 -7
  104. package/build/lib/types/HAPServiceConfigType.d.ts +0 -26
  105. package/build/lib/types/HAPServiceNodeType.d.ts +0 -38
  106. package/build/lib/types/HAPStatusConfigType.d.ts +0 -5
  107. package/build/lib/types/HAPStatusNodeType.d.ts +0 -12
  108. package/build/lib/types/HostType.d.ts +0 -5
  109. package/build/lib/types/NodeType.d.ts +0 -3
  110. package/build/lib/types/PublishTimersType.d.ts +0 -4
  111. package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.d.ts +0 -5
  112. package/build/lib/types/hap-nodejs/HapCategories.d.ts +0 -41
  113. package/build/lib/types/storage/SerializedHostType.d.ts +0 -5
  114. package/build/lib/types/storage/StorageType.d.ts +0 -8
  115. package/build/lib/utils/AccessoryUtils.d.ts +0 -1
  116. package/build/lib/utils/BridgeUtils.d.ts +0 -1
  117. package/build/lib/utils/CharacteristicUtils.d.ts +0 -1
  118. package/build/lib/utils/CharacteristicUtils2.d.ts +0 -1
  119. package/build/lib/utils/NodeStatusUtils.d.ts +0 -17
  120. package/build/lib/utils/ServiceUtils.d.ts +0 -1
  121. package/build/lib/utils/ServiceUtils2.d.ts +0 -1
  122. package/build/lib/utils/index.d.ts +0 -1
  123. package/build/nodes/bridge.d.ts +0 -1
  124. package/build/nodes/nrchkb.d.ts +0 -1
  125. package/build/nodes/service.d.ts +0 -1
  126. package/build/nodes/service2.d.ts +0 -1
  127. package/build/nodes/standalone.d.ts +0 -1
  128. package/build/nodes/status.d.ts +0 -1
@@ -1,32 +1,780 @@
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.nrchkb-checkbox-label {
368
+ display: inline-flex !important;
369
+ gap: 8px;
370
+ line-height: 1.35;
371
+ }
372
+
373
+ .nrchkb-sidebar-pairing label.nrchkb-sidebar-setting input {
374
+ width: 16px;
375
+ height: 16px;
376
+ margin: 0 !important;
377
+ }
378
+
379
+ .nrchkb-sidebar-pairing label.nrchkb-sidebar-setting.nrchkb-checkbox-label input[type="checkbox"] {
380
+ width: 36px;
381
+ height: 22px;
382
+ }
383
+
384
+ .nrchkb-sidebar-pairing label.nrchkb-sidebar-setting span {
385
+ display: block;
386
+ min-width: 0;
387
+ }
388
+
389
+ .nrchkb-sidebar-pairing-card {
390
+ display: grid;
391
+ grid-template-rows: auto auto auto 1fr;
392
+ margin: 0 0 12px;
393
+ padding: 10px;
394
+ border: 1px solid var(--nrchkb-section-border);
395
+ border-radius: 7px;
396
+ background: var(--nrchkb-section-bg);
397
+ container-type: inline-size;
398
+ min-width: 0;
399
+ }
400
+
401
+ .nrchkb-sidebar-pairing-card h3 {
402
+ margin: 0 0 8px;
403
+ font-size: 14px;
404
+ line-height: 1.35;
405
+ }
406
+
407
+ .nrchkb-sidebar-pairing-list {
408
+ display: grid;
409
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
410
+ gap: 10px;
411
+ }
412
+
413
+ .nrchkb-sidebar-pairing .nrchkb-pairing-setup-card {
414
+ margin: 0 auto 8px;
415
+ width: min(100%, 220px);
416
+ }
417
+
418
+ .nrchkb-sidebar-meta {
419
+ display: grid;
420
+ grid-template-columns: auto minmax(0, 1fr);
421
+ gap: 3px 8px;
422
+ min-height: 66px;
423
+ margin: 0 0 8px;
424
+ color: var(--nrchkb-muted-text);
425
+ font-size: 12px;
426
+ line-height: 1.35;
427
+ }
428
+
429
+ .nrchkb-sidebar-meta dt {
430
+ margin: 0;
431
+ font-weight: 600;
432
+ }
433
+
434
+ .nrchkb-sidebar-meta dd {
435
+ min-width: 0;
436
+ margin: 0;
437
+ overflow-wrap: anywhere;
438
+ }
439
+
440
+ .nrchkb-sidebar-accessories {
441
+ margin: 8px 0 0;
442
+ padding: 8px 0 0;
443
+ border-top: 1px solid var(--nrchkb-section-border);
444
+ }
445
+
446
+ .nrchkb-sidebar-accessories-title {
447
+ margin: 0 0 5px;
448
+ color: var(--nrchkb-muted-text);
449
+ font-size: 12px;
450
+ font-weight: 600;
451
+ line-height: 1.35;
452
+ }
453
+
454
+ .nrchkb-sidebar-accessories ul {
455
+ margin: 0;
456
+ padding: 0;
457
+ color: var(--nrchkb-muted-text);
458
+ line-height: 1.4;
459
+ list-style: none;
460
+ }
461
+
462
+ .nrchkb-sidebar-accessories li {
463
+ margin: 0 0 7px;
464
+ }
465
+
466
+ .nrchkb-sidebar-accessory-name {
467
+ display: block;
468
+ color: var(--red-ui-primary-text-color, #333);
469
+ font-weight: 600;
470
+ }
471
+
472
+ .nrchkb-sidebar-accessory-meta {
473
+ display: block;
474
+ overflow-wrap: anywhere;
475
+ font-size: 12px;
476
+ }
477
+
478
+ .nrchkb-sidebar-pairing-control {
479
+ margin: 0 0 10px;
480
+ }
481
+
482
+ .nrchkb-sidebar-toolbar {
483
+ display: flex;
484
+ justify-content: flex-end;
485
+ gap: 6px;
486
+ padding: 6px;
487
+ border-top: 1px solid var(--nrchkb-section-border);
488
+ }
489
+
490
+ .fa-nrchkb::before {
491
+ content: "";
492
+ display: inline-block;
493
+ width: 1.25em;
494
+ height: 1.25em;
495
+ vertical-align: -20%;
496
+ background-image: url("icons/node-red-contrib-homekit-bridged/nrchkb.png");
497
+ background-size: contain;
498
+ background-position: center;
499
+ background-repeat: no-repeat;
500
+ }
501
+
502
+ .nrchkb-help {
503
+ --nrchkb-help-border: var(--red-ui-secondary-border-color, #d8d8d8);
504
+ --nrchkb-help-bg: var(--red-ui-secondary-background, #fff);
505
+ --nrchkb-help-heading-bg: var(--red-ui-tertiary-background, #f6f6f6);
506
+ --nrchkb-help-muted: var(--red-ui-secondary-text-color, #666);
507
+ }
508
+
509
+ .nrchkb-help .nrchkb-help-section {
510
+ margin: 10px 0;
511
+ border: 1px solid var(--nrchkb-help-border);
512
+ border-radius: 7px;
513
+ background: var(--nrchkb-help-bg);
514
+ overflow: hidden;
515
+ }
516
+
517
+ .nrchkb-help .nrchkb-help-section > h4 {
518
+ margin: 0;
519
+ padding: 7px 10px;
520
+ font-size: 13px;
521
+ line-height: 1.35;
522
+ background: var(--nrchkb-help-heading-bg);
523
+ }
524
+
525
+ .nrchkb-help .nrchkb-help-section > h4 i {
526
+ width: 14px;
527
+ margin-right: 6px;
528
+ color: var(--nrchkb-help-muted);
529
+ text-align: center;
530
+ }
531
+
532
+ .nrchkb-help .nrchkb-help-section-body {
533
+ padding: 8px 10px;
534
+ }
535
+
536
+ .nrchkb-help .nrchkb-help-section-body > :first-child {
537
+ margin-top: 0;
538
+ }
539
+
540
+ .nrchkb-help .nrchkb-help-section-body > :last-child {
541
+ margin-bottom: 0;
542
+ }
543
+
544
+ .nrchkb-help .nrchkb-advertiser-dynamic {
545
+ margin: 8px 0;
546
+ padding: 7px 9px;
547
+ border-left: 3px solid var(--red-ui-primary-background, #8f0000);
548
+ border-radius: 4px;
549
+ background: color-mix(in srgb, var(--red-ui-tertiary-background, #f6f6f6) 78%, transparent);
550
+ }
551
+
552
+ .nrchkb-editor .nrchkb-characteristic-item {
553
+ overflow: hidden;
554
+ white-space: normal;
555
+ }
556
+
557
+ .nrchkb-editor .nrchkb-characteristic-row {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 10px;
561
+ min-width: 0;
562
+ margin-bottom: 8px;
563
+ }
564
+
565
+ .nrchkb-editor .nrchkb-characteristic-fields {
566
+ margin-top: 8px;
567
+ min-width: 0;
568
+ }
569
+
570
+ .nrchkb-editor .nrchkb-characteristic-label {
571
+ flex: 0 1 120px;
572
+ min-width: 98px;
573
+ max-width: min(120px, 42%);
574
+ padding-left: 10px;
575
+ text-align: left;
576
+ }
577
+
578
+ .nrchkb-editor .nrchkb-characteristic-label i,
579
+ .nrchkb-editor .nrchkb-characteristic-property-label i,
580
+ .nrchkb-editor .properties-accordion h3 i {
581
+ width: 14px;
582
+ color: var(--nrchkb-muted-text);
583
+ text-align: center;
584
+ }
585
+
586
+ .nrchkb-editor .nrchkb-characteristic-row input,
587
+ .nrchkb-editor .nrchkb-characteristic-row select,
588
+ .nrchkb-editor .nrchkb-characteristic-fields input,
589
+ .nrchkb-editor .nrchkb-characteristic-fields select {
590
+ flex: 1 1 0;
591
+ min-width: 0;
592
+ width: auto !important;
593
+ }
594
+
595
+ .nrchkb-editor .nrchkb-characteristic-fields .property-validValueRanges {
596
+ flex: 1 1 0;
597
+ min-width: 0;
598
+ max-width: 100%;
599
+ }
600
+
601
+ .nrchkb-editor #node-input-customCharacteristics-container {
602
+ width: 100%;
603
+ min-width: 0;
604
+ min-height: 150px;
605
+ }
606
+
607
+ .nrchkb-editor ol#node-input-customCharacteristics-container .red-ui-typedInput-container {
608
+ flex: 1;
609
+ }
610
+
611
+ .nrchkb-editor .node-input-customCharacteristics-container-row {
612
+ display: block;
613
+ min-width: 0;
614
+ }
615
+
616
+ .nrchkb-editor .red-ui-editableList,
617
+ .nrchkb-editor .red-ui-editableList-border,
618
+ .nrchkb-editor .red-ui-editableList-container,
619
+ .nrchkb-editor .red-ui-editableList-list,
620
+ .nrchkb-editor .red-ui-editableList-list > li {
621
+ min-width: 0;
622
+ max-width: 100%;
623
+ }
624
+
625
+ .nrchkb-editor .ui-slider .ui-slider-handle {
626
+ background: var(--nrchkb-accent);
627
+ }
628
+
629
+ @supports not (color: color-mix(in srgb, #000 50%, #fff)) {
630
+ .nrchkb-editor {
631
+ --nrchkb-toggle-off: var(--red-ui-tertiary-background, #f6f6f6);
632
+ --nrchkb-toggle-border: var(--red-ui-secondary-border-color, #d8d8d8);
633
+ }
634
+
635
+ .nrchkb-editor .nrchkb-section,
636
+ .nrchkb-editor .nrchkb-nested-panel,
637
+ .nrchkb-editor .nrchkb-section > summary,
638
+ .nrchkb-editor .nrchkb-advertiser-recommendation,
639
+ .nrchkb-help .nrchkb-advertiser-dynamic {
640
+ background: var(--red-ui-secondary-background, #fff);
641
+ }
642
+
643
+ .nrchkb-editor input[type="checkbox"]:checked {
644
+ border-color: #1f8f3d;
645
+ }
646
+
647
+ .nrchkb-editor .nrchkb-checkbox-label i {
648
+ color: var(--nrchkb-muted-text);
649
+ }
650
+
651
+ .nrchkb-editor .nrchkb-checkbox-label input[type="checkbox"]:checked + span i {
652
+ color: #1f6f35;
653
+ }
654
+ }
655
+
656
+ @container (max-width: 520px) {
657
+ .nrchkb-editor .form-row {
658
+ align-items: stretch;
659
+ flex-direction: column;
660
+ gap: 4px;
661
+ }
662
+
663
+ .nrchkb-editor .form-row > label:first-child {
664
+ flex: 0 0 auto;
665
+ max-width: 100%;
666
+ min-width: 0;
667
+ width: auto;
668
+ }
669
+
670
+ .nrchkb-editor .form-row > input[type="text"],
671
+ .nrchkb-editor .form-row > input[type="number"],
672
+ .nrchkb-editor .form-row > input:not([type]),
673
+ .nrchkb-editor .form-row > select,
674
+ .nrchkb-editor .form-row > .red-ui-typedInput-container,
675
+ .nrchkb-editor .form-row > div[style*="inline-flex"] {
676
+ flex: 0 0 auto;
677
+ width: 100% !important;
678
+ }
679
+
680
+ .nrchkb-editor .nrchkb-checkbox-row {
681
+ display: block;
682
+ }
683
+
684
+ .nrchkb-editor .nrchkb-characteristic-row {
685
+ align-items: stretch;
686
+ flex-direction: column;
687
+ gap: 4px;
688
+ }
689
+
690
+ .nrchkb-editor .nrchkb-characteristic-label {
691
+ flex: 0 0 auto;
692
+ max-width: 100%;
693
+ min-width: 0;
694
+ padding-left: 0;
695
+ }
696
+
697
+ .nrchkb-editor .nrchkb-pairing-card {
698
+ align-items: flex-start;
699
+ flex-direction: column;
700
+ }
701
+ }
702
+
703
+ @supports not (container-type: inline-size) {
704
+ @media (max-width: 520px) {
705
+ .nrchkb-editor .form-row {
706
+ align-items: stretch;
707
+ flex-direction: column;
708
+ gap: 4px;
709
+ }
710
+
711
+ .nrchkb-editor .form-row > label:first-child {
712
+ flex: 0 0 auto;
713
+ max-width: 100%;
714
+ min-width: 0;
715
+ width: auto;
716
+ }
717
+
718
+ .nrchkb-editor .form-row > input[type="text"],
719
+ .nrchkb-editor .form-row > input[type="number"],
720
+ .nrchkb-editor .form-row > input:not([type]),
721
+ .nrchkb-editor .form-row > select,
722
+ .nrchkb-editor .form-row > .red-ui-typedInput-container,
723
+ .nrchkb-editor .form-row > div[style*="inline-flex"] {
724
+ flex: 0 0 auto;
725
+ width: 100% !important;
726
+ }
727
+
728
+ .nrchkb-editor .nrchkb-checkbox-row {
729
+ display: block;
730
+ }
731
+
732
+ .nrchkb-editor .nrchkb-characteristic-row {
733
+ align-items: stretch;
734
+ flex-direction: column;
735
+ gap: 4px;
736
+ }
737
+
738
+ .nrchkb-editor .nrchkb-characteristic-label {
739
+ flex: 0 0 auto;
740
+ max-width: 100%;
741
+ min-width: 0;
742
+ padding-left: 0;
743
+ }
744
+
745
+ .nrchkb-editor .nrchkb-pairing-card {
746
+ align-items: flex-start;
747
+ flex-direction: column;
748
+ }
749
+ }
750
+ }
751
+
752
+ .nrchkb-editor .alert {
3
753
  position: relative;
4
754
  padding: .75rem 1.25rem;
5
755
  margin-bottom: 1rem;
6
- border: 1px solid transparent;
756
+ border: 1px solid var(--nrchkb-section-border);
7
757
  border-radius: .25rem;
8
- margin-right: 60px;
758
+ margin-right: 0;
759
+ color: var(--red-ui-primary-text-color, #333);
760
+ background: var(--red-ui-tertiary-background, #f6f6f6);
9
761
  }
10
762
 
11
- .alert-warning {
12
- color: #856404;
13
- background-color: #fff3cd;
14
- border-color: #ffeeba;
763
+ .nrchkb-editor .alert-warning {
764
+ color: var(--nrchkb-warning-text);
765
+ background: var(--nrchkb-warning-bg);
766
+ border-color: var(--nrchkb-warning-border);
767
+ }
768
+
769
+ .nrchkb-editor .alert-info {
770
+ color: var(--nrchkb-info-text);
771
+ background: var(--nrchkb-info-bg);
772
+ border-color: var(--nrchkb-info-border);
15
773
  }
16
774
  </style>
17
775
 
18
776
  <script type="text/javascript">
19
- const initExperimental = function () {
20
- //NRCHKB Custom Characteristics
21
- $.ajax({
22
- url: 'nrchkb/config',
23
- dataType: 'json',
24
- async: false,
25
- success: function (data) {
26
- nrchkbConfig = data
27
- },
28
- })
29
-
777
+ const registerNRCHKBConfigNode = function () {
30
778
  RED.nodes.registerType('nrchkb', {
31
779
  category: 'Apple HomeKit',
32
780
  icon: 'nrchkb.png',
@@ -43,6 +791,9 @@
43
791
  labelStyle: function () {
44
792
  return 'node_label_italic'
45
793
  },
794
+ onadd: function () {
795
+ applyDefaultNodeDocumentation(this, 'nrchkb')
796
+ },
46
797
  oneditsave: function () {
47
798
  const self = this
48
799
  saveCustomCharacteristics(self)
@@ -55,56 +806,75 @@
55
806
  types: ["json"]
56
807
  })*/
57
808
 
58
- $('#node-input-customCharacteristics-container').css('min-height', '150px').css('min-width', '550px').editableList({
809
+ $('#node-input-customCharacteristics-container').editableList({
59
810
  addItem: function (container, i, opt) {
60
811
  $('.properties-accordion').accordion('option', 'active', false)
61
812
 
62
813
  const {name, UUID, ...props} = opt
63
814
 
64
- container.css({
65
- overflow: 'hidden',
66
- whiteSpace: 'nowrap'
67
- })
815
+ container.addClass('nrchkb-characteristic-item')
68
816
 
69
817
  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)
818
+ const row1 = $('<div/>', {class: 'nrchkb-characteristic-row'}).appendTo(fragment)
819
+ const row2 = $('<div/>', {class: 'nrchkb-characteristic-row'}).appendTo(fragment)
820
+ const row3 = $('<div/>', {class: 'nrchkb-characteristic-fields'}).appendTo(fragment)
821
+ const fieldIdPrefix = 'node-input-customCharacteristic-' + i + '-'
822
+ const createFieldId = function (name) {
823
+ return fieldIdPrefix + name
824
+ }
825
+ const setIconLabelText = function (target, iconClass, text) {
826
+ target.empty()
827
+ $('<i/>', {class: 'fa ' + iconClass}).appendTo(target)
828
+ target.append(document.createTextNode(' ' + text))
829
+ }
830
+ const createPropertyLabel = function (attrs, iconClass, text) {
831
+ const label = $('<label/>', attrs)
832
+ setIconLabelText(label, iconClass, text)
833
+ return label
834
+ }
73
835
 
74
- $('<div/>', {
75
- style: 'display:inline-block;text-align:left; width:120px; padding-left:10px; box-sizing:border-box;',
836
+ const uuidFieldId = createFieldId('uuid')
837
+ const uuidLabel = $('<label/>', {
838
+ class: 'nrchkb-characteristic-label',
839
+ for: uuidFieldId,
76
840
  required: 'required'
77
841
  })
78
- .text('UUID ')
79
842
  .appendTo(row1)
80
- $('<input/>', {class: 'property-uuid', type: 'text'})
843
+ setIconLabelText(uuidLabel, 'fa-key', 'UUID')
844
+ $('<input/>', {class: 'property-uuid', id: uuidFieldId, type: 'text'})
81
845
  .val(UUID ? UUID : uuidv4)
82
846
  .appendTo(row1)
83
847
 
84
- $('<div/>', {
85
- style: 'display:inline-block;text-align:left; width:120px; padding-left:10px; box-sizing:border-box;',
848
+ const nameFieldId = createFieldId('name')
849
+ const nameLabel = $('<label/>', {
850
+ class: 'nrchkb-characteristic-label',
851
+ for: nameFieldId,
86
852
  required: 'required'
87
853
  })
88
- .text('Name ')
89
854
  .appendTo(row2)
90
- $('<input/>', {class: 'property-name', type: 'text'})
855
+ setIconLabelText(nameLabel, 'fa-tag', 'Name')
856
+ $('<input/>', {class: 'property-name', id: nameFieldId, type: 'text'})
91
857
  .val(name)
92
858
  .appendTo(row2)
93
859
 
94
860
  const row3_properties_accordion = $('<div/>', {class: 'properties-accordion'})
95
861
  .appendTo(row3)
96
862
 
97
- $('<h3/>').text('Properties').appendTo(row3_properties_accordion)
863
+ $('<h3/>')
864
+ .append($('<i/>', {class: 'fa fa-sliders'}), document.createTextNode(' Properties'))
865
+ .appendTo(row3_properties_accordion)
98
866
 
99
867
  const row3_properties = $('<div/>', {class: 'properties'}).appendTo(row3_properties_accordion)
100
868
 
101
869
  const formatRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
102
- $('<label/>', {
103
- class: 'form-row',
104
- htmlFor: 'property-format'
105
- }).text('Format *').appendTo(formatRow)
870
+ const formatFieldId = createFieldId('format')
871
+ createPropertyLabel({
872
+ class: 'nrchkb-characteristic-property-label',
873
+ for: formatFieldId
874
+ }, 'fa-code', 'Format *').appendTo(formatRow)
106
875
  const formatInput = $('<select/>', {
107
876
  class: 'property-format',
877
+ id: formatFieldId,
108
878
  required: 'required'
109
879
  }).appendTo(formatRow)
110
880
  $('<option/>').val(undefined).text('Choose...').appendTo(formatInput)
@@ -123,8 +893,12 @@
123
893
  formatInput.val(props.format)
124
894
 
125
895
  const unitRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
126
- $('<label/>', {class: 'form-row', htmlFor: 'property-unit'}).text('Unit').appendTo(unitRow)
127
- const unitSelect = $('<select/>', {class: 'property-unit'}).appendTo(unitRow)
896
+ const unitFieldId = createFieldId('unit')
897
+ createPropertyLabel({
898
+ class: 'nrchkb-characteristic-property-label',
899
+ for: unitFieldId
900
+ }, 'fa-balance-scale', 'Unit').appendTo(unitRow)
901
+ const unitSelect = $('<select/>', {class: 'property-unit', id: unitFieldId}).appendTo(unitRow)
128
902
  $('<option/>').val(undefined).text('Choose...').appendTo(unitSelect)
129
903
  $('<option/>').val('celsius').text('CELSIUS').appendTo(unitSelect)
130
904
  $('<option/>').val('percentage').text('PERCENTAGE').appendTo(unitSelect)
@@ -134,12 +908,14 @@
134
908
  unitSelect.val(props.unit)
135
909
 
136
910
  const permsRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
137
- $('<label/>', {
138
- class: 'form-row',
139
- htmlFor: 'property-perms'
140
- }).text('Permissions').appendTo(permsRow)
911
+ const permsFieldId = createFieldId('perms')
912
+ createPropertyLabel({
913
+ class: 'nrchkb-characteristic-property-label',
914
+ for: permsFieldId
915
+ }, 'fa-lock', 'Permissions').appendTo(permsRow)
141
916
  const permsSelect = $('<select/>', {
142
917
  class: 'property-perms',
918
+ id: permsFieldId,
143
919
  multiple: 'multiple'
144
920
  }).appendTo(permsRow)
145
921
  $('<option/>').val('pr').text('PAIRED_READ / READ').appendTo(permsSelect)
@@ -152,93 +928,113 @@
152
928
  permsSelect.val(props.perms)
153
929
 
154
930
  const evRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
155
- $('<label/>', {
156
- class: 'form-row',
157
- htmlFor: 'property-ev'
158
- }).text('Event Notifications').appendTo(evRow)
931
+ const evFieldId = createFieldId('ev')
932
+ createPropertyLabel({
933
+ class: 'nrchkb-characteristic-property-label',
934
+ for: evFieldId
935
+ }, 'fa-bell-o', 'Event Notifications').appendTo(evRow)
159
936
  $('<input/>', {
160
937
  class: 'property-ev',
938
+ id: evFieldId,
161
939
  type: 'checkbox',
162
940
  checked: props.ev
163
941
  }).appendTo(evRow)
164
942
 
165
943
  const descriptionRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
166
- $('<label/>', {
167
- class: 'form-row',
168
- htmlFor: 'property-description'
169
- }).text('Description').appendTo(descriptionRow)
944
+ const descriptionFieldId = createFieldId('description')
945
+ createPropertyLabel({
946
+ class: 'nrchkb-characteristic-property-label',
947
+ for: descriptionFieldId
948
+ }, 'fa-align-left', 'Description').appendTo(descriptionRow)
170
949
  $('<input/>', {
171
950
  class: 'property-description',
951
+ id: descriptionFieldId,
172
952
  type: 'text'
173
953
  }).appendTo(descriptionRow).val(props.description)
174
954
 
175
955
  const minValueRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
176
- $('<label/>', {
177
- class: 'form-row',
178
- htmlFor: 'property-minValue'
179
- }).text('Minimum Value').appendTo(minValueRow)
956
+ const minValueFieldId = createFieldId('minValue')
957
+ createPropertyLabel({
958
+ class: 'nrchkb-characteristic-property-label',
959
+ for: minValueFieldId
960
+ }, 'fa-sort-numeric-asc', 'Minimum Value').appendTo(minValueRow)
180
961
  $('<input/>', {
181
962
  class: 'property-minValue',
963
+ id: minValueFieldId,
182
964
  type: 'number'
183
965
  }).appendTo(minValueRow).val(props.minValue)
184
966
 
185
967
  const maxValueRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
186
- $('<label/>', {
187
- class: 'form-row',
188
- htmlFor: 'property-maxValue'
189
- }).text('Maximum Value').appendTo(maxValueRow)
968
+ const maxValueFieldId = createFieldId('maxValue')
969
+ createPropertyLabel({
970
+ class: 'nrchkb-characteristic-property-label',
971
+ for: maxValueFieldId
972
+ }, 'fa-sort-numeric-desc', 'Maximum Value').appendTo(maxValueRow)
190
973
  $('<input/>', {
191
974
  class: 'property-maxValue',
975
+ id: maxValueFieldId,
192
976
  type: 'number'
193
977
  }).appendTo(maxValueRow).val(props.maxValue)
194
978
 
195
979
  const minStepRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
196
- $('<label/>', {
197
- class: 'form-row',
198
- htmlFor: 'property-minStep'
199
- }).text('Minimum Step').appendTo(minStepRow)
980
+ const minStepFieldId = createFieldId('minStep')
981
+ createPropertyLabel({
982
+ class: 'nrchkb-characteristic-property-label',
983
+ for: minStepFieldId
984
+ }, 'fa-step-forward', 'Minimum Step').appendTo(minStepRow)
200
985
  $('<input/>', {
201
986
  class: 'property-minStep',
987
+ id: minStepFieldId,
202
988
  type: 'number'
203
989
  }).appendTo(minStepRow).val(props.minStep)
204
990
 
205
991
  const maxLenRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
206
- $('<label/>', {
207
- class: 'form-row',
208
- htmlFor: 'property-maxLen'
209
- }).text('Maximum Length').appendTo(maxLenRow)
992
+ const maxLenFieldId = createFieldId('maxLen')
993
+ createPropertyLabel({
994
+ class: 'nrchkb-characteristic-property-label',
995
+ for: maxLenFieldId
996
+ }, 'fa-text-width', 'Maximum Length').appendTo(maxLenRow)
210
997
  $('<input/>', {
211
998
  class: 'property-maxLen',
999
+ id: maxLenFieldId,
212
1000
  type: 'number'
213
1001
  }).appendTo(maxLenRow).val(props.maxLen)
214
1002
 
215
1003
  const maxDataLenRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
216
- $('<label/>', {
217
- class: 'form-row',
218
- htmlFor: 'property-maxDataLen'
219
- }).text('Maximum Data Length').appendTo(maxDataLenRow)
1004
+ const maxDataLenFieldId = createFieldId('maxDataLen')
1005
+ createPropertyLabel({
1006
+ class: 'nrchkb-characteristic-property-label',
1007
+ for: maxDataLenFieldId
1008
+ }, 'fa-database', 'Maximum Data Length').appendTo(maxDataLenRow)
220
1009
  $('<input/>', {
221
1010
  class: 'property-maxDataLen',
1011
+ id: maxDataLenFieldId,
222
1012
  type: 'number'
223
1013
  }).appendTo(maxDataLenRow).val(props.maxDataLen)
224
1014
 
225
1015
  const validValuesRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
226
- $('<label/>', {
227
- class: 'form-row',
228
- htmlFor: 'property-validValues'
229
- }).text('Valid Values').appendTo(validValuesRow)
1016
+ const validValuesFieldId = createFieldId('validValues')
1017
+ createPropertyLabel({
1018
+ class: 'nrchkb-characteristic-property-label',
1019
+ for: validValuesFieldId
1020
+ }, 'fa-check-square-o', 'Valid Values').appendTo(validValuesRow)
230
1021
  $('<input/>', {
231
1022
  class: 'property-validValues',
1023
+ id: validValuesFieldId,
232
1024
  type: 'text'
233
1025
  }).appendTo(validValuesRow).val(props.validValues)
234
1026
 
235
1027
  const validValueRangesRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
236
- const validValueRangesLabel = $('<label/>', {
237
- class: 'form-row property-validValueRanges-label',
238
- htmlFor: 'property-validValueRanges'
239
- }).text('Valid Value Ranges: ').appendTo(validValueRangesRow)
1028
+ const validValueRangesFieldId = createFieldId('validValueRanges')
1029
+ const validValueRangesLabelId = validValueRangesFieldId + '-label'
1030
+ const validValueRangesLabel = createPropertyLabel({
1031
+ class: 'nrchkb-characteristic-property-label property-validValueRanges-label',
1032
+ id: validValueRangesLabelId,
1033
+ }, 'fa-sliders', 'Valid Value Ranges').appendTo(validValueRangesRow)
240
1034
  const validValueRangesSlider = $('<div/>', {
241
1035
  class: 'property-validValueRanges',
1036
+ id: validValueRangesFieldId,
1037
+ 'aria-labelledby': validValueRangesLabelId,
242
1038
  type: 'text'
243
1039
  }).appendTo(validValueRangesRow)
244
1040
 
@@ -248,18 +1044,22 @@
248
1044
  max: 500,
249
1045
  values: props.validValueRanges,
250
1046
  slide: function (event, ui) {
251
- validValueRangesLabel.text('Valid Value Ranges: [' + ui.values[0] + ', ' + ui.values[1] + ']')
1047
+ setIconLabelText(validValueRangesLabel, 'fa-sliders', 'Valid Value Ranges: [' + ui.values[0] + ', ' + ui.values[1] + ']')
252
1048
  }
253
1049
  })
254
- validValueRangesLabel.text('Valid Value Ranges: [' + validValueRangesSlider.slider('values', 0) + ', ' + validValueRangesSlider.slider('values', 1) + ']')
1050
+ validValueRangesSlider.find('.ui-slider-handle')
1051
+ .attr('aria-labelledby', validValueRangesLabelId)
1052
+ setIconLabelText(validValueRangesLabel, 'fa-sliders', 'Valid Value Ranges: [' + validValueRangesSlider.slider('values', 0) + ', ' + validValueRangesSlider.slider('values', 1) + ']')
255
1053
 
256
1054
  const adminOnlyAccessRow = $('<div/>', {class: 'form-row'}).appendTo(row3_properties)
257
- $('<label/>', {
258
- class: 'form-row',
259
- htmlFor: 'property-adminOnlyAccess'
260
- }).text('Admin Only Access').appendTo(adminOnlyAccessRow)
1055
+ const adminOnlyAccessFieldId = createFieldId('adminOnlyAccess')
1056
+ createPropertyLabel({
1057
+ class: 'nrchkb-characteristic-property-label',
1058
+ for: adminOnlyAccessFieldId
1059
+ }, 'fa-user-secret', 'Admin Only Access').appendTo(adminOnlyAccessRow)
261
1060
  const adminOnlyAccessSelect = $('<select/>', {
262
1061
  class: 'property-adminOnlyAccess',
1062
+ id: adminOnlyAccessFieldId,
263
1063
  multiple: 'multiple'
264
1064
  }).appendTo(adminOnlyAccessRow)
265
1065
  $('<option/>').val(0).text('READ').appendTo(adminOnlyAccessSelect)
@@ -279,24 +1079,47 @@
279
1079
  sortable: true
280
1080
  })
281
1081
 
282
- for (let i = 0; i < nrchkbConfig.customCharacteristics.length; i++) {
283
- const customCharacteristic = nrchkbConfig.customCharacteristics[i]
1082
+ const customCharacteristics = Array.isArray(nrchkbConfig.customCharacteristics)
1083
+ ? nrchkbConfig.customCharacteristics
1084
+ : []
1085
+
1086
+ for (let i = 0; i < customCharacteristics.length; i++) {
1087
+ const customCharacteristic = customCharacteristics[i]
284
1088
  $('#node-input-customCharacteristics-container').editableList('addItem', customCharacteristic)
285
1089
  }
286
1090
  },
287
1091
  oneditresize: function (size) {
288
- const rows = $('#dialog-form>div:not(.node-input-customCharacteristics-container-row)')
1092
+ const rows = $('#dialog-form .nrchkb-section > summary, #dialog-form .nrchkb-section-body > .form-row:not(.node-input-customCharacteristics-container-row)')
289
1093
  let height = size.height
290
1094
  for (let i = 0; i < rows.length; i++) {
291
1095
  height -= $(rows[i]).outerHeight(true)
292
1096
  }
293
- const editorRow = $('#dialog-form>div.node-input-customCharacteristics-container-row')
1097
+ const editorRow = $('#dialog-form .node-input-customCharacteristics-container-row')
294
1098
  height -= (parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom')))
295
1099
  height += 16
296
- $('#node-input-customCharacteristics-container').editableList('height', height)
1100
+ $('#node-input-customCharacteristics-container').editableList('height', Math.max(150, height))
297
1101
  }
298
1102
  })
299
1103
  }
1104
+
1105
+ const initNRCHKBConfigNode = function () {
1106
+ //NRCHKB Custom Characteristics
1107
+ $.ajax({
1108
+ url: 'nrchkb/config',
1109
+ dataType: 'json',
1110
+ })
1111
+ .done(function (data) {
1112
+ nrchkbConfig = data || {}
1113
+ if (!Array.isArray(nrchkbConfig.customCharacteristics)) {
1114
+ nrchkbConfig.customCharacteristics = []
1115
+ }
1116
+ })
1117
+ .fail(function () {
1118
+ nrchkbConfig = {customCharacteristics: []}
1119
+ RED.notify('Unable to load NRCHKB custom characteristics.', 'warning')
1120
+ })
1121
+ .always(registerNRCHKBConfigNode)
1122
+ }
300
1123
  </script>
301
1124
 
302
1125
  <script type="text/javascript">
@@ -304,15 +1127,18 @@
304
1127
  return port !== 1880 && port >= 1 && port <= 65535 && port === port.toString()
305
1128
  }
306
1129
 
1130
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor helper consumed by node templates loaded after nrchkb.html.
307
1131
  const isValueDefined = function (value) {
308
1132
  return 'undefined' === typeof value ? false : null !== value
309
1133
  }
310
1134
 
311
- let serviceTypes
312
- let accessoryCategories
1135
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor data consumed by service editors loaded after nrchkb.html.
1136
+ let serviceTypes = {}
1137
+ let accessoryCategories = {}
1138
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor data consumed by config and service editors loaded after nrchkb.html.
313
1139
  let nrchkbVersion = '0.0.0'
314
- let nrchkbExperimental = false
315
1140
  let nrchkbConfig = {}
1141
+ let nrchkbAdvertiserRecommendation = null
316
1142
 
317
1143
  //HomeKit Service Types
318
1144
  $.getJSON('nrchkb/service/types', function (data) {
@@ -328,27 +1154,116 @@
328
1154
  $.ajax({
329
1155
  url: 'nrchkb/info',
330
1156
  dataType: 'json',
331
- async: false,
332
- success: function (data) {
1157
+ })
1158
+ .done(function (data) {
333
1159
  nrchkbVersion = data.version
334
- nrchkbExperimental = data.experimental
1160
+ })
1161
+ .fail(function () {
1162
+ RED.notify('Unable to load NRCHKB version information.', 'warning')
1163
+ })
1164
+ .always(initNRCHKBConfigNode)
335
1165
 
336
- if (nrchkbExperimental) {
337
- initExperimental()
338
- }
339
- },
1166
+ const renderAdvertiserRecommendation = function (recommendation) {
1167
+ if (!recommendation || !recommendation.recommended) {
1168
+ return 'Recommendation unavailable.'
1169
+ }
1170
+
1171
+ const caveats = Array.isArray(recommendation.caveats)
1172
+ ? recommendation.caveats
1173
+ : []
1174
+ const caveatText = caveats.length
1175
+ ? ' ' + caveats.join(' ')
1176
+ : ''
1177
+ const reason = recommendation.reason || ''
1178
+ const reasonText = reason.replace(/[.!?]+$/, '') + '.'
1179
+
1180
+ return (
1181
+ '<strong>' +
1182
+ recommendation.title +
1183
+ '</strong>: ' +
1184
+ reasonText +
1185
+ caveatText
1186
+ )
1187
+ }
1188
+
1189
+ const updateAdvertiserHelpTemplates = function () {
1190
+ const renderedRecommendation = renderAdvertiserRecommendation(
1191
+ nrchkbAdvertiserRecommendation,
1192
+ )
1193
+
1194
+ $('script[data-help-name="homekit-bridge"], script[data-help-name="homekit-standalone"]').each(function () {
1195
+ this.innerHTML = this.innerHTML.replace(
1196
+ /<p class="nrchkb-advertiser-dynamic">[\s\S]*?<\/p>/,
1197
+ '<p class="nrchkb-advertiser-dynamic">' +
1198
+ renderedRecommendation +
1199
+ '</p>',
1200
+ )
1201
+ })
1202
+ }
1203
+
1204
+ const applyAdvertiserRecommendation = function (select) {
1205
+ const selectAdvertiser = $(select)
1206
+
1207
+ if (!selectAdvertiser.length) {
1208
+ return
1209
+ }
1210
+
1211
+ const recommendation = nrchkbAdvertiserRecommendation
1212
+
1213
+ selectAdvertiser.find('option').each(function () {
1214
+ const option = $(this)
1215
+ const originalText = option.data('nrchkbOriginalText') || option.text()
1216
+ option.data('nrchkbOriginalText', originalText)
1217
+ option.text(originalText)
1218
+ })
1219
+
1220
+ selectAdvertiser
1221
+ .closest('.form-row')
1222
+ .next('.nrchkb-advertiser-recommendation')
1223
+ .remove()
1224
+
1225
+ if (!recommendation || !recommendation.recommended) {
1226
+ return
1227
+ }
1228
+
1229
+ const recommendedOption = selectAdvertiser.find(
1230
+ 'option[value="' + recommendation.recommended + '"]',
1231
+ )
1232
+
1233
+ if (recommendedOption.length) {
1234
+ recommendedOption.text(
1235
+ recommendedOption.data('nrchkbOriginalText') + ' (recommended)',
1236
+ )
1237
+ }
1238
+
1239
+ selectAdvertiser
1240
+ .closest('.form-row')
1241
+ .after(
1242
+ '<div class="nrchkb-advertiser-recommendation">' +
1243
+ renderAdvertiserRecommendation(recommendation) +
1244
+ '</div>',
1245
+ )
1246
+ }
1247
+
1248
+ $.getJSON('nrchkb/advertiser/recommendation', function (data) {
1249
+ nrchkbAdvertiserRecommendation = data
1250
+ updateAdvertiserHelpTemplates()
1251
+ applyAdvertiserRecommendation('#node-config-input-advertiser')
340
1252
  })
341
1253
 
1254
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor validator consumed by node templates loaded after nrchkb.html.
342
1255
  const versionValidator = function (value) {
343
1256
  return value ? /^(\d+\.)?(\d+\.)?(\.|\d+)$/.test(value) : true
344
1257
  }
345
1258
 
1259
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor validator consumed by config node templates loaded after nrchkb.html.
346
1260
  const hostNameValidator = function (value) {
347
1261
  return value ? /^[^.]{1,64}$/.test(value) : false
348
1262
  }
349
1263
 
1264
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor validator consumed by service templates loaded after nrchkb.html.
350
1265
  const cameraConfigRequiredField = function (value) {
351
- return 'CameraControl' === this.serviceName ? (value || '').toString().trim() : true
1266
+ return ['CameraControl', 'Camera'].includes(this.serviceName) ? (value || '').toString().trim() : true
352
1267
  }
353
1268
 
354
1269
  const saveCustomCharacteristics = function (self) {
@@ -440,6 +1355,7 @@
440
1355
  return `${a}${b}${c}${d}-${e}${f}${g}${h}`
441
1356
  }
442
1357
 
1358
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor validator consumed by config node templates loaded after nrchkb.html.
443
1359
  const validatePinCode = function (value) {
444
1360
  if (!value) {
445
1361
  return false
@@ -451,23 +1367,743 @@
451
1367
 
452
1368
  return !forbiddenPinCodes.includes(value.replaceAll('-', ''))
453
1369
  }
454
- </script>
455
1370
 
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;
1371
+ // biome-ignore lint/correctness/noUnusedVariables: Shared editor helper consumed by node templates loaded after nrchkb.html.
1372
+ const sortSelectOptionsByLabel = function (select, options) {
1373
+ const selectElement = $(select)
1374
+ const sortedOptions = (options || []).sort(function (left, right) {
1375
+ return left.text.localeCompare(right.text, undefined, {
1376
+ numeric: true,
1377
+ sensitivity: 'base'
1378
+ })
1379
+ })
1380
+
1381
+ sortedOptions.forEach(function (option) {
1382
+ selectElement.append(option.element)
1383
+ })
1384
+ }
1385
+
1386
+ const nrchkbLegacyDefaultNodeInfo = {
1387
+ 'homekit-status': [
1388
+ '# HomeKit Status',
1389
+ '',
1390
+ '> [!NOTE]',
1391
+ '> Any input message reads the selected HomeKit service and emits a serialized service description.',
1392
+ ].join('\n'),
1393
+ nrchkb: [
1394
+ '# NRCHKB Custom Characteristics',
1395
+ '',
1396
+ '> [!CAUTION]',
1397
+ '> Custom characteristics should be used only when a standard HAP characteristic cannot represent the device state.',
1398
+ ].join('\n'),
1399
+ }
1400
+
1401
+ const nrchkbDefaultNodeInfo = {
1402
+ 'homekit-service2': [
1403
+ '# HomeKit Service 2',
1404
+ '',
1405
+ '> [!IMPORTANT]',
1406
+ '> Send characteristic updates as `msg.payload` keys using HomeKit characteristic names, for example `{\"On\": true}`.',
1407
+ '',
1408
+ '> [!NOTE]',
1409
+ '> Use the node Help sidebar for the full characteristic, plugin, and migration reference.',
1410
+ ].join('\n'),
1411
+ 'homekit-bridge': [
1412
+ '# HomeKit Bridge',
1413
+ '',
1414
+ '> [!IMPORTANT]',
1415
+ '> Keep the bridge name, PIN, serial number, and model stable after pairing. HomeKit uses them as accessory identity.',
1416
+ '',
1417
+ '> [!WARNING]',
1418
+ '> Leave insecure requests disabled unless you are deliberately testing with a trusted local client.',
1419
+ ].join('\n'),
1420
+ 'homekit-standalone': [
1421
+ '# HomeKit Standalone Accessory',
1422
+ '',
1423
+ '> [!IMPORTANT]',
1424
+ '> Standalone accessories publish independently. Prefer a bridge for most multi-service setups to reduce mDNS load.',
1425
+ '',
1426
+ '> [!WARNING]',
1427
+ '> Leave insecure requests disabled unless you are deliberately testing with a trusted local client.',
1428
+ ].join('\n'),
1429
+ 'homekit-service': [
1430
+ '# HomeKit Service',
1431
+ '',
1432
+ '> [!WARNING]',
1433
+ '> This legacy node is kept for existing flows. Use `homekit-service2` for new work and migrate when practical.',
1434
+ '',
1435
+ '> [!NOTE]',
1436
+ '> The migration button prepares changes in the editor. Click Done and then Deploy to commit them.',
1437
+ ].join('\n'),
1438
+ 'homekit-status': [
1439
+ '# HomeKit Status',
1440
+ '',
1441
+ 'Reads the selected `homekit-service` or `homekit-service2` node and outputs a serialized description of its current HomeKit service shape.',
1442
+ '',
1443
+ '> [!NOTE]',
1444
+ '> Any input message triggers a fresh read. The incoming payload is not used as a command.',
1445
+ '',
1446
+ 'Use this node for diagnostics, dashboards, migration checks, or flows that need to inspect which characteristics and values a service currently exposes.',
1447
+ '',
1448
+ 'The output includes the selected service metadata, characteristics, properties, and current values where the runtime can read them.',
1449
+ ].join('\n'),
1450
+ 'homekit-unifi-controller': [
1451
+ '# UniFi Controller',
1452
+ '',
1453
+ '> [!IMPORTANT]',
1454
+ '> Use a dedicated local UniFi account with the minimum permissions needed to read Protect camera data.',
1455
+ '',
1456
+ '> [!WARNING]',
1457
+ '> Allow self-signed certificates only for controllers you administer and trust on your local network.',
1458
+ ].join('\n'),
1459
+ 'homekit-plugin-instance': [
1460
+ '# HomeKit Plugin Instance',
1461
+ '',
1462
+ '> [!NOTE]',
1463
+ '> This internal config node lets Node-RED track plugin dependencies used by NRCHKB service nodes.',
1464
+ ].join('\n'),
1465
+ nrchkb: [
1466
+ '# NRCHKB Custom Characteristics',
1467
+ '',
1468
+ 'Defines custom HAP characteristics that can be attached to HomeKit services when the standard HAP catalog does not contain the value you need.',
1469
+ '',
1470
+ '> [!CAUTION]',
1471
+ '> Custom characteristics should be used only when a standard HAP characteristic cannot represent the device state.',
1472
+ '',
1473
+ '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.',
1474
+ '',
1475
+ '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.',
1476
+ ].join('\n'),
1477
+ }
1478
+
1479
+ const applyDefaultNodeDocumentation = function (node, type) {
1480
+ if ((!node.info || node.info === nrchkbLegacyDefaultNodeInfo[type]) && nrchkbDefaultNodeInfo[type]) {
1481
+ node.info = nrchkbDefaultNodeInfo[type]
1482
+ node.dirty = true
1483
+ return true
460
1484
  }
461
- .ui-slider .ui-slider-handle {
462
- background: #ad1625;
1485
+
1486
+ return false
1487
+ }
1488
+
1489
+ const backfillDefaultNodeDocumentation = function (nodes) {
1490
+ let changed = false
1491
+ const applyToNode = function (node) {
1492
+ if (node && applyDefaultNodeDocumentation(node, node.type)) {
1493
+ changed = true
1494
+ }
463
1495
  }
464
- </style>
465
1496
 
466
- <div class="form-row" style="margin-bottom:0;">
467
- <label><i class="fa fa-list"></i> Custom Characteristics</label>
468
- </div>
1497
+ if (Array.isArray(nodes)) {
1498
+ nodes.forEach(applyToNode)
1499
+ } else if (nodes) {
1500
+ applyToNode(nodes)
1501
+ } else if (RED.nodes) {
1502
+ RED.nodes.eachNode(applyToNode)
469
1503
 
470
- <div class="form-row node-input-customCharacteristics-container-row">
471
- <ol id="node-input-customCharacteristics-container"></ol>
1504
+ if (typeof RED.nodes.eachConfig === 'function') {
1505
+ RED.nodes.eachConfig(applyToNode)
1506
+ }
1507
+ }
1508
+
1509
+ if (changed && RED.view && typeof RED.view.redraw === 'function') {
1510
+ RED.view.redraw(true)
1511
+ }
1512
+ }
1513
+
1514
+ if (RED.events) {
1515
+ RED.events.on('flows:loaded', function () {
1516
+ setTimeout(backfillDefaultNodeDocumentation, 0)
1517
+ })
1518
+
1519
+ RED.events.on('nodes:add', function (node) {
1520
+ setTimeout(function () {
1521
+ backfillDefaultNodeDocumentation(node)
1522
+ }, 0)
1523
+ })
1524
+ }
1525
+
1526
+ window.NRCHKBPairingQR = (function () {
1527
+ const stateByHost = {}
1528
+ const settingsStorageKey = 'nrchkb.pairingQR.settings'
1529
+ let initialized = false
1530
+ let refreshTimer
1531
+ let refreshRequestTimer
1532
+ let sidebarContent
1533
+ let sidebarList
1534
+ let sidebarStatus
1535
+ let settings = {
1536
+ autoShowWhenPairingAvailable: true,
1537
+ }
1538
+ const prefersReducedMotion = function () {
1539
+ return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches
1540
+ }
1541
+ const slideDownElement = function (element, duration) {
1542
+ if (prefersReducedMotion()) {
1543
+ element.stop(true, true).show()
1544
+ return
1545
+ }
1546
+
1547
+ element.stop(true, true).slideDown(duration)
1548
+ }
1549
+ const slideUpElement = function (element, duration, onComplete) {
1550
+ if (prefersReducedMotion()) {
1551
+ element.stop(true, true).hide()
1552
+ if (onComplete) {
1553
+ onComplete()
1554
+ }
1555
+ return
1556
+ }
1557
+
1558
+ element.stop(true, true).slideUp(duration, onComplete)
1559
+ }
1560
+ const text = function (key, fallback) {
1561
+ const keys = [
1562
+ 'node-red-contrib-homekit-bridged/' + key,
1563
+ 'node-red-contrib-homekit-bridged:' + key,
1564
+ ]
1565
+ try {
1566
+ for (let i = 0; i < keys.length; i++) {
1567
+ const value = RED._(keys[i])
1568
+ if (value && value !== keys[i] && value !== key) {
1569
+ return value
1570
+ }
1571
+ }
1572
+ } catch (_error) {
1573
+ }
1574
+ return fallback
1575
+ }
1576
+
1577
+ const loadSettings = function () {
1578
+ try {
1579
+ const storedSettings = JSON.parse(localStorage.getItem(settingsStorageKey) || '{}')
1580
+ settings = {
1581
+ ...settings,
1582
+ ...storedSettings,
1583
+ autoShowWhenPairingAvailable: storedSettings.autoShowWhenPairingAvailable !== false,
1584
+ }
1585
+ } catch (_error) {
1586
+ settings.autoShowWhenPairingAvailable = true
1587
+ }
1588
+ }
1589
+
1590
+ const saveSettings = function () {
1591
+ try {
1592
+ localStorage.setItem(settingsStorageKey, JSON.stringify(settings))
1593
+ } catch (_error) {
1594
+ }
1595
+ }
1596
+
1597
+ const resolveHostId = function (node) {
1598
+ if (!node) {
1599
+ return ''
1600
+ }
1601
+
1602
+ if ((node.type === 'homekit-service' || node.type === 'homekit-service2') && node.bridge) {
1603
+ return node.bridge
1604
+ }
1605
+
1606
+ if ((node.type === 'homekit-service' || node.type === 'homekit-service2') && node.accessoryId) {
1607
+ return node.accessoryId
1608
+ }
1609
+
1610
+ if (node.type === 'homekit-status' && node.serviceNodeId) {
1611
+ return resolveHostId(RED.nodes.node(node.serviceNodeId))
1612
+ }
1613
+
1614
+ return ''
1615
+ }
1616
+
1617
+ const fetchHostState = function (hostId) {
1618
+ if (!hostId) {
1619
+ return Promise.resolve(undefined)
1620
+ }
1621
+
1622
+ return new Promise(function (resolve) {
1623
+ $.ajax({
1624
+ dataType: 'json',
1625
+ url: 'nrchkb/bridge/' + encodeURIComponent(hostId) + '/pairing',
1626
+ })
1627
+ .done(function (data) {
1628
+ stateByHost[hostId] = data
1629
+ resolve(data)
1630
+ })
1631
+ .fail(function (xhr) {
1632
+ const data = xhr.responseJSON || {
1633
+ paired: false,
1634
+ published: false,
1635
+ status: xhr.status === 404 ? 'missing' : 'error',
1636
+ }
1637
+ stateByHost[hostId] = data
1638
+ resolve(data)
1639
+ })
1640
+ })
1641
+ }
1642
+
1643
+ const getPairingHostNodes = function () {
1644
+ const hosts = []
1645
+ const hostTypes = {
1646
+ 'homekit-bridge': true,
1647
+ 'homekit-standalone': true,
1648
+ }
1649
+
1650
+ if (typeof RED.nodes.eachConfig === 'function') {
1651
+ RED.nodes.eachConfig(function (node) {
1652
+ if (node && hostTypes[node.type]) {
1653
+ hosts.push(node)
1654
+ }
1655
+ })
1656
+ }
1657
+
1658
+ if (hosts.length === 0) {
1659
+ return [
1660
+ ...RED.nodes.filterNodes({type: 'homekit-bridge'}),
1661
+ ...RED.nodes.filterNodes({type: 'homekit-standalone'}),
1662
+ ]
1663
+ }
1664
+
1665
+ return hosts
1666
+ }
1667
+
1668
+ const getCandidateNodes = function () {
1669
+ return [
1670
+ ...RED.nodes.filterNodes({type: 'homekit-service'}),
1671
+ ...RED.nodes.filterNodes({type: 'homekit-service2'}),
1672
+ ...RED.nodes.filterNodes({type: 'homekit-status'}),
1673
+ ]
1674
+ }
1675
+
1676
+ const getReferencingNodes = function (hostId) {
1677
+ return getCandidateNodes().filter(function (node) {
1678
+ return resolveHostId(node) === hostId
1679
+ })
1680
+ }
1681
+
1682
+ const formatPinCode = function (formattedPinCode) {
1683
+ return formattedPinCode ? formattedPinCode.top + ' ' + formattedPinCode.bottom : ''
1684
+ }
1685
+
1686
+ const createPairingSetupCard = function (bridgeState) {
1687
+ const setupCard = $('<div/>', {class: 'nrchkb-pairing-setup-card'})
1688
+ $('<img/>', {
1689
+ alt: text('qr.imageAlt', 'HomeKit pairing QR code'),
1690
+ class: 'nrchkb-pairing-qr',
1691
+ src: bridgeState.qrCodeDataUrl,
1692
+ }).appendTo(setupCard)
1693
+ return setupCard
1694
+ }
1695
+
1696
+ const isPairingAvailable = function (bridgeState) {
1697
+ return !!bridgeState && bridgeState.published && !bridgeState.paired && !!bridgeState.qrCodeDataUrl
1698
+ }
1699
+
1700
+ const nodeDisplayName = function (node) {
1701
+ return node.name || node.serviceName || node.id || text('qr.unknownAccessory', 'Accessory')
1702
+ }
1703
+
1704
+ const serviceTypeName = function (node) {
1705
+ if (!node) {
1706
+ return ''
1707
+ }
1708
+
1709
+ if (node.type === 'homekit-status' && node.serviceNodeId) {
1710
+ return serviceTypeName(RED.nodes.node(node.serviceNodeId))
1711
+ }
1712
+
1713
+ return node.serviceName || node.service || node.type || ''
1714
+ }
1715
+
1716
+ const hostCategoryName = function (hostNode) {
1717
+ if (!hostNode || hostNode.type !== 'homekit-standalone') {
1718
+ return ''
1719
+ }
1720
+
1721
+ return accessoryCategories[hostNode.accessoryCategory] || hostNode.accessoryCategory || ''
1722
+ }
1723
+
1724
+ const hostTypeName = function (hostNode) {
1725
+ return hostNode && hostNode.type === 'homekit-standalone' ? 'Standalone accessory' : 'Bridge'
1726
+ }
1727
+
1728
+ const appendMeta = function (container, items) {
1729
+ const meta = $('<dl/>', {class: 'nrchkb-sidebar-meta'}).appendTo(container)
1730
+ items.forEach(function (item) {
1731
+ if (!item.value) {
1732
+ return
1733
+ }
1734
+ $('<dt/>', {text: item.label}).appendTo(meta)
1735
+ $('<dd/>', {text: item.value}).appendTo(meta)
1736
+ })
1737
+ return meta
1738
+ }
1739
+
1740
+ const renderAccessoryListItem = function (list, node) {
1741
+ const item = $('<li/>').appendTo(list)
1742
+ $('<span/>', {
1743
+ class: 'nrchkb-sidebar-accessory-name',
1744
+ text: nodeDisplayName(node),
1745
+ }).appendTo(item)
1746
+ $('<span/>', {
1747
+ class: 'nrchkb-sidebar-accessory-meta',
1748
+ text: 'id: ' + node.id + ', service: ' + serviceTypeName(node),
1749
+ }).appendTo(item)
1750
+ }
1751
+
1752
+ const renderSidebarCardContent = function (card, hostNode, hostState) {
1753
+ card.empty()
1754
+ $('<h3/>', {
1755
+ text: hostState.bridgeName || hostNode.name || hostNode.bridgeName || hostNode.id,
1756
+ }).appendTo(card)
1757
+ appendMeta(card, [
1758
+ {label: 'Type', value: hostTypeName(hostNode)},
1759
+ {label: 'ID', value: hostNode.id},
1760
+ {label: 'Category', value: hostCategoryName(hostNode)},
1761
+ ])
1762
+ createPairingSetupCard(hostState).appendTo(card)
1763
+
1764
+ const accessories = getReferencingNodes(hostNode.id)
1765
+ const accessorySection = $('<div/>', {class: 'nrchkb-sidebar-accessories'}).appendTo(card)
1766
+ $('<p/>', {
1767
+ class: 'nrchkb-sidebar-accessories-title',
1768
+ text: text('qr.accessories', 'Accessories'),
1769
+ }).appendTo(accessorySection)
1770
+
1771
+ if (accessories.length === 0) {
1772
+ $('<p/>', {
1773
+ class: 'nrchkb-pairing-message',
1774
+ text: text('qr.noAccessories', 'No flow nodes reference this pairing host.'),
1775
+ }).appendTo(accessorySection)
1776
+ } else {
1777
+ const list = $('<ul/>').appendTo(accessorySection)
1778
+ accessories.forEach(function (node) {
1779
+ renderAccessoryListItem(list, node)
1780
+ })
1781
+ }
1782
+ }
1783
+
1784
+ const createSidebarCard = function (hostNode, hostState) {
1785
+ const card = $('<div/>', {
1786
+ class: 'nrchkb-sidebar-pairing-card',
1787
+ 'data-host-id': hostNode.id,
1788
+ role: 'listitem',
1789
+ })
1790
+ renderSidebarCardContent(card, hostNode, hostState)
1791
+ return card
1792
+ }
1793
+
1794
+ const sidebarCardSignature = function (hostNode, hostState) {
1795
+ return JSON.stringify({
1796
+ accessories: getReferencingNodes(hostNode.id).map(function (node) {
1797
+ return {
1798
+ id: node.id,
1799
+ name: nodeDisplayName(node),
1800
+ serviceType: serviceTypeName(node),
1801
+ }
1802
+ }),
1803
+ category: hostCategoryName(hostNode),
1804
+ hostId: hostNode.id,
1805
+ hostName: hostState.bridgeName || hostNode.name || hostNode.bridgeName || hostNode.id,
1806
+ hostType: hostTypeName(hostNode),
1807
+ qrCodeDataUrl: hostState.qrCodeDataUrl,
1808
+ })
1809
+ }
1810
+
1811
+ const findSidebarCard = function (hostId) {
1812
+ return sidebarList.children('.nrchkb-sidebar-pairing-card').filter(function () {
1813
+ return $(this).attr('data-host-id') === hostId
1814
+ })
1815
+ }
1816
+
1817
+ const removeSidebarCard = function (card, onComplete) {
1818
+ if (card.data('nrchkbRemoving')) {
1819
+ return
1820
+ }
1821
+
1822
+ card.data('nrchkbRemoving', true)
1823
+ slideUpElement(card, 180, function () {
1824
+ card.remove()
1825
+ if (onComplete) {
1826
+ onComplete()
1827
+ }
1828
+ })
1829
+ }
1830
+
1831
+ const updateEmptySidebarState = function () {
1832
+ if (sidebarList.children('.nrchkb-sidebar-pairing-card').length === 0) {
1833
+ sidebarStatus.text(text('qr.noUnpaired', 'Nothing to be paired.'))
1834
+ }
1835
+ }
1836
+
1837
+ const refreshSidebar = function (options) {
1838
+ options = options || {}
1839
+
1840
+ if (!sidebarList || !sidebarStatus) {
1841
+ return Promise.resolve()
1842
+ }
1843
+
1844
+ const pairingHosts = getPairingHostNodes()
1845
+
1846
+ if (pairingHosts.length === 0) {
1847
+ sidebarList.children('.nrchkb-sidebar-pairing-card').each(function () {
1848
+ removeSidebarCard($(this), updateEmptySidebarState)
1849
+ })
1850
+ updateEmptySidebarState()
1851
+ return Promise.resolve()
1852
+ }
1853
+
1854
+ return Promise.all(pairingHosts.map(function (hostNode) {
1855
+ return fetchHostState(hostNode.id).then(function (hostState) {
1856
+ return {hostNode, hostState}
1857
+ })
1858
+ })).then(function (results) {
1859
+ const available = results.filter(function (result) {
1860
+ return isPairingAvailable(result.hostState)
1861
+ })
1862
+ const availableById = {}
1863
+
1864
+ available.forEach(function (result) {
1865
+ availableById[result.hostNode.id] = result
1866
+ })
1867
+
1868
+ sidebarList.children('.nrchkb-sidebar-pairing-card').each(function () {
1869
+ const card = $(this)
1870
+ const hostId = card.attr('data-host-id')
1871
+
1872
+ if (!availableById[hostId]) {
1873
+ removeSidebarCard(card, updateEmptySidebarState)
1874
+ }
1875
+ })
1876
+
1877
+ if (available.length === 0) {
1878
+ updateEmptySidebarState()
1879
+ } else {
1880
+ sidebarStatus.text('')
1881
+ available.forEach(function (result) {
1882
+ const signature = sidebarCardSignature(result.hostNode, result.hostState)
1883
+ const card = findSidebarCard(result.hostNode.id)
1884
+
1885
+ if (card.length > 0) {
1886
+ if (card.attr('data-signature') !== signature) {
1887
+ renderSidebarCardContent(card, result.hostNode, result.hostState)
1888
+ card.attr('data-signature', signature)
1889
+ }
1890
+ return
1891
+ }
1892
+
1893
+ const newCard = createSidebarCard(result.hostNode, result.hostState)
1894
+ .attr('data-signature', signature)
1895
+ .hide()
1896
+ sidebarList.append(newCard)
1897
+ slideDownElement(newCard, 180)
1898
+ })
1899
+ }
1900
+
1901
+ if (options.show && available.length > 0 && settings.autoShowWhenPairingAvailable && RED.sidebar) {
1902
+ RED.sidebar.show('nrchkb')
1903
+ }
1904
+ })
1905
+ }
1906
+
1907
+ const requestRefreshSidebar = function (delay, options) {
1908
+ clearTimeout(refreshRequestTimer)
1909
+ refreshRequestTimer = setTimeout(function () {
1910
+ refreshSidebar(options)
1911
+ }, delay)
1912
+ }
1913
+
1914
+ const registerSidebar = function () {
1915
+ sidebarContent = $('<div/>', {class: 'nrchkb-editor nrchkb-sidebar-pairing'})
1916
+ const section = $('<details/>', {class: 'nrchkb-section', open: true}).appendTo(sidebarContent)
1917
+ $('<summary/>')
1918
+ .append($('<i/>', {class: 'fa fa-qrcode'}))
1919
+ .append(' ')
1920
+ .append($('<span/>', {text: text('qr.sectionTitle', 'Pairing QR Code')}))
1921
+ .appendTo(section)
1922
+ const sectionBody = $('<div/>', {class: 'nrchkb-section-body'}).appendTo(section)
1923
+ const autoShowLabel = $('<label/>', {
1924
+ class: 'nrchkb-checkbox-label nrchkb-sidebar-setting nrchkb-sidebar-pairing-control',
1925
+ }).appendTo(sectionBody)
1926
+ $('<input/>', {
1927
+ checked: settings.autoShowWhenPairingAvailable,
1928
+ type: 'checkbox',
1929
+ })
1930
+ .on('change', function () {
1931
+ settings.autoShowWhenPairingAvailable = this.checked
1932
+ saveSettings()
1933
+ })
1934
+ .appendTo(autoShowLabel)
1935
+ $('<span/>')
1936
+ .append($('<i/>', {class: 'fa fa-eye'}))
1937
+ .append(' ')
1938
+ .append(text('qr.autoShowWhenPairingAvailable', 'Auto show if something needs pairing'))
1939
+ .appendTo(autoShowLabel)
1940
+ sidebarStatus = $('<p/>', {
1941
+ class: 'nrchkb-sidebar-pairing-status',
1942
+ 'aria-live': 'polite',
1943
+ }).appendTo(sectionBody)
1944
+ sidebarList = $('<div/>', {
1945
+ class: 'nrchkb-sidebar-pairing-list',
1946
+ role: 'list',
1947
+ }).appendTo(sectionBody)
1948
+
1949
+ const toolbar = $('<div/>', {class: 'nrchkb-sidebar-toolbar'})
1950
+ $('<button/>', {
1951
+ class: 'red-ui-button red-ui-button-small',
1952
+ 'aria-label': text('qr.refresh', 'Refresh'),
1953
+ title: text('qr.refresh', 'Refresh'),
1954
+ type: 'button',
1955
+ })
1956
+ .append($('<i/>', {class: 'fa fa-refresh'}))
1957
+ .on('click', function () {
1958
+ refreshSidebar()
1959
+ })
1960
+ .appendTo(toolbar)
1961
+
1962
+ RED.sidebar.addTab({
1963
+ action: 'nrchkb:show-pairing-tab',
1964
+ content: sidebarContent,
1965
+ enableOnEdit: true,
1966
+ iconClass: 'fa fa-nrchkb',
1967
+ id: 'nrchkb',
1968
+ label: text('qr.sidebarTitle', 'NRCHKB'),
1969
+ name: text('qr.sidebarTitle', 'NRCHKB'),
1970
+ pinned: true,
1971
+ toolbar,
1972
+ })
1973
+
1974
+ RED.actions.add('nrchkb:show-pairing-tab', function () {
1975
+ RED.sidebar.show('nrchkb')
1976
+ refreshSidebar()
1977
+ })
1978
+ }
1979
+
1980
+ const renderHostEditor = function (hostId, selector) {
1981
+ const container = $(selector)
1982
+ container.empty().append($('<p/>', {
1983
+ class: 'nrchkb-pairing-message',
1984
+ text: text('qr.loading', 'Loading pairing state...'),
1985
+ }))
1986
+
1987
+ fetchHostState(hostId).then(function (hostState) {
1988
+ container.empty()
1989
+
1990
+ if (!hostState || hostState.status === 'missing' || !hostState.published) {
1991
+ container.append($('<p/>', {
1992
+ class: 'nrchkb-pairing-message',
1993
+ text: text('qr.deployFirst', 'Deploy this bridge or accessory to generate a pairing QR code.'),
1994
+ }))
1995
+ return
1996
+ }
1997
+
1998
+ if (hostState.paired) {
1999
+ container.append($('<p/>', {
2000
+ class: 'nrchkb-pairing-message',
2001
+ text: text('qr.alreadyPaired', 'This bridge or accessory is already paired.'),
2002
+ }))
2003
+ return
2004
+ }
2005
+
2006
+ const card = $('<div/>', {class: 'nrchkb-pairing-card'})
2007
+ createPairingSetupCard(hostState).appendTo(card)
2008
+ const details = $('<div/>', {class: 'nrchkb-pairing-details'}).appendTo(card)
2009
+ $('<p/>', {
2010
+ class: 'nrchkb-pairing-state',
2011
+ text: text('qr.ready', 'Ready to pair'),
2012
+ }).appendTo(details)
2013
+ $('<span/>', {
2014
+ class: 'nrchkb-pairing-code',
2015
+ text: formatPinCode(hostState.formattedPinCode),
2016
+ }).appendTo(details)
2017
+ $('<span/>', {
2018
+ class: 'nrchkb-pairing-uri',
2019
+ text: hostState.setupUri,
2020
+ }).appendTo(details)
2021
+ container.append(card)
2022
+ })
2023
+ }
2024
+
2025
+ const init = function () {
2026
+ if (initialized) {
2027
+ return
2028
+ }
2029
+
2030
+ initialized = true
2031
+
2032
+ loadSettings()
2033
+ registerSidebar()
2034
+
2035
+ if (RED.events) {
2036
+ RED.events.on('deploy', function () {
2037
+ requestRefreshSidebar(1500, {show: true})
2038
+ })
2039
+
2040
+ ;['flows:loaded', 'nodes:add', 'nodes:change', 'nodes:remove'].forEach(function (eventName) {
2041
+ RED.events.on(eventName, function () {
2042
+ requestRefreshSidebar(250)
2043
+ })
2044
+ })
2045
+ }
2046
+
2047
+ refreshTimer = setInterval(refreshSidebar, 10000)
2048
+ window.addEventListener('beforeunload', function () {
2049
+ clearInterval(refreshTimer)
2050
+ clearTimeout(refreshRequestTimer)
2051
+ })
2052
+ setTimeout(function () {
2053
+ refreshSidebar({show: true})
2054
+ }, 1000)
2055
+ }
2056
+
2057
+ return {
2058
+ init,
2059
+ refreshSidebar,
2060
+ renderBridgeEditor: renderHostEditor,
2061
+ renderHostEditor,
2062
+ }
2063
+ })()
2064
+ RED.NRCHKBPairingQR = window.NRCHKBPairingQR
2065
+
2066
+ setTimeout(function () {
2067
+ if (RED.NRCHKBPairingQR) {
2068
+ RED.NRCHKBPairingQR.init()
2069
+ }
2070
+ }, 1000)
2071
+ </script>
2072
+
2073
+ <script data-template-name="nrchkb" type="text/x-red">
2074
+ <div class="nrchkb-editor">
2075
+ <details class="nrchkb-section" open>
2076
+ <summary><i class="fa fa-list-alt"></i> Custom Characteristics</summary>
2077
+ <div class="nrchkb-section-body">
2078
+ <div class="form-row node-input-customCharacteristics-container-row">
2079
+ <ol id="node-input-customCharacteristics-container"></ol>
2080
+ </div>
2081
+ </div>
2082
+ </details>
472
2083
  </div>
473
- </script>
2084
+ </script>
2085
+
2086
+ <script data-help-name="nrchkb" type="text/markdown">
2087
+ # NRCHKB Custom Characteristics
2088
+
2089
+ Defines custom HAP characteristics that can be attached to HomeKit services when the standard HAP catalog does not contain the value you need.
2090
+
2091
+ > [!CAUTION]
2092
+ > Prefer standard HAP characteristics whenever possible. Custom characteristics may not be shown or interpreted consistently by every HomeKit client.
2093
+
2094
+ ## Characteristic Identity
2095
+
2096
+ - **Name**: Display name used by NRCHKB when selecting or reporting the characteristic.
2097
+ - **UUID**: Stable HAP UUID for the custom characteristic. Keep it unchanged once HomeKit clients have seen it.
2098
+
2099
+ ## Value Shape
2100
+
2101
+ - **Format**: HAP value format such as boolean, integer, float, string, or data.
2102
+ - **Unit**: Optional HomeKit unit metadata for numeric values.
2103
+ - **Minimum / Maximum / Step**: Numeric constraints exposed to HomeKit clients.
2104
+ - **Valid Values**: Optional list of allowed values for enumerated characteristics.
2105
+
2106
+ ## Permissions
2107
+
2108
+ 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.
2109
+ </script>