node-red-contrib-knx-ultimate 6.3.6 → 6.3.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.
- package/CHANGELOG.md +9 -0
- package/nodes/hue-config.js +12 -2
- package/nodes/knxUltimateHueController.html +279 -31
- package/nodes/locales/de/knxUltimateHueController.html +8 -1
- package/nodes/locales/de/knxUltimateHueController.json +4 -1
- package/nodes/locales/en/knxUltimateHueController.html +8 -1
- package/nodes/locales/en/knxUltimateHueController.json +4 -1
- package/nodes/locales/es/knxUltimateHueController.html +8 -1
- package/nodes/locales/es/knxUltimateHueController.json +4 -1
- package/nodes/locales/fr/knxUltimateHueController.html +8 -1
- package/nodes/locales/fr/knxUltimateHueController.json +4 -1
- package/nodes/locales/it/knxUltimateHueController.html +8 -1
- package/nodes/locales/it/knxUltimateHueController.json +4 -1
- package/nodes/locales/zh-CN/knxUltimateHueController.html +8 -1
- package/nodes/locales/zh-CN/knxUltimateHueController.json +4 -1
- package/package.json +1 -1
- package/resources/hueControllerEditorSelection.js +32 -0
- package/resources/hueControllerProfiles.js +75 -29
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 6.3.8** - August 2026<br/>
|
|
10
|
+
|
|
11
|
+
- **HUE Controller**: fixed Light and grouped-light mapping tabs remaining hidden when Node-RED emits temporary empty KNX/Hue config-selector values during editor initialization; saved gateways now survive bootstrap and remount, while only an explicit user selection of `none` hides the mapping tabs.<br/>
|
|
12
|
+
|
|
13
|
+
**Version 6.3.7** - August 2026<br/>
|
|
14
|
+
|
|
15
|
+
- **HUE Controller**: improved gateway-dependent editor visibility, preserving all KNX mappings and restoring the correct tab layout when gateways are deselected or reselected; obsolete headings and decorative images were removed.<br/>
|
|
16
|
+
- **HUE Controller**: added clearer Hue Bridge loading/offline feedback, kept saved mappings visible while disabling unavailable device discovery, and restored the complete previous configuration when the editor is cancelled after previewing another Hue resource.<br/>
|
|
17
|
+
|
|
9
18
|
**Version 6.3.6** - August 2026<br/>
|
|
10
19
|
|
|
11
20
|
- **HUE Controller**: grouped lights now always show the complete Light mapping editor when KNX is configured, without checking child-light capabilities.<br/>
|
package/nodes/hue-config.js
CHANGED
|
@@ -367,16 +367,26 @@ module.exports = (RED) => {
|
|
|
367
367
|
node.sysLogger?.warn(`KNXUltimateHue: getResources force refresh failed ${error.message}`)
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
|
-
if (node.hueAllResources === undefined) return
|
|
371
370
|
if (_rtype === 'hue_controller') {
|
|
371
|
+
const ready = node.linkStatus === 'connected' && Array.isArray(node.hueAllResources)
|
|
372
|
+
if (!ready) {
|
|
373
|
+
return {
|
|
374
|
+
devices: [],
|
|
375
|
+
ready: false,
|
|
376
|
+
connectionStatus: node.linkStatus
|
|
377
|
+
}
|
|
378
|
+
}
|
|
372
379
|
const resourceResults = await Promise.all(HUE_CONTROLLER_RESOURCE_TYPES.map(async (resourceType) => {
|
|
373
380
|
const result = await node.getResources(resourceType)
|
|
374
381
|
return [resourceType, Array.isArray(result?.devices) ? result.devices : []]
|
|
375
382
|
}))
|
|
376
383
|
return {
|
|
377
|
-
devices: buildHueControllerResourceCatalog(Object.fromEntries(resourceResults))
|
|
384
|
+
devices: buildHueControllerResourceCatalog(Object.fromEntries(resourceResults)),
|
|
385
|
+
ready: true,
|
|
386
|
+
connectionStatus: node.linkStatus
|
|
378
387
|
}
|
|
379
388
|
}
|
|
389
|
+
if (node.hueAllResources === undefined) return
|
|
380
390
|
// Returns capitalized string
|
|
381
391
|
function capStr (s) {
|
|
382
392
|
if (typeof s !== 'string') return ''
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script>
|
|
2
2
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
|
|
3
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/hueControllerEditorSelection.js"></script>
|
|
3
4
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/hueControllerProfiles.js"></script>
|
|
4
5
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/hueControllerMigration.js"></script>
|
|
5
6
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/hueControllerMigrationDialog.js"></script>
|
|
@@ -37,8 +38,34 @@
|
|
|
37
38
|
// gateways exist) and for "Add new..." (when none exist).
|
|
38
39
|
const KNX_EMPTY_SERVER_VALUES = new Set(['', 'none', '_add_', '__none__']);
|
|
39
40
|
const HUE_EMPTY_SERVER_VALUES = new Set(['', 'none', '_add_', '__none__']);
|
|
41
|
+
const normalizeKnxServerSelection = (value) => {
|
|
42
|
+
const normalized = value === undefined || value === null ? '' : String(value).trim();
|
|
43
|
+
return KNX_EMPTY_SERVER_VALUES.has(normalized.toLowerCase()) ? '' : normalized;
|
|
44
|
+
};
|
|
45
|
+
const normalizeHueServerSelection = (value) => {
|
|
46
|
+
const normalized = value === undefined || value === null ? '' : String(value).trim();
|
|
47
|
+
return HUE_EMPTY_SERVER_VALUES.has(normalized.toLowerCase()) ? '' : normalized;
|
|
48
|
+
};
|
|
40
49
|
const TUTORIALS_URL = 'https://www.youtube.com/playlist?list=PL9Yh1bjbLAYrU8PsVhW4xzEug2WtVFv3E';
|
|
41
50
|
const profileBundle = window.KNXUltimateHueControllerProfiles;
|
|
51
|
+
const editorSelection = window.KNXUltimateHueControllerEditorSelection;
|
|
52
|
+
const shouldPreserveStoredSelection = editorSelection && typeof editorSelection.shouldPreserveStoredSelection === 'function'
|
|
53
|
+
? editorSelection.shouldPreserveStoredSelection
|
|
54
|
+
: (event, nextValue, storedValue, emptyValues) => {
|
|
55
|
+
const programmaticChange = !event || !event.originalEvent;
|
|
56
|
+
const normalizedNext = nextValue === undefined || nextValue === null ? '' : String(nextValue).trim().toLowerCase();
|
|
57
|
+
const normalizedStored = storedValue === undefined || storedValue === null ? '' : String(storedValue).trim().toLowerCase();
|
|
58
|
+
return programmaticChange && emptyValues.has(normalizedNext) && !emptyValues.has(normalizedStored);
|
|
59
|
+
};
|
|
60
|
+
const resolveSelectedOrStoredSelection = editorSelection && typeof editorSelection.resolveSelectedOrStoredSelection === 'function'
|
|
61
|
+
? editorSelection.resolveSelectedOrStoredSelection
|
|
62
|
+
: (selectedValue, storedValue, emptyValues) => {
|
|
63
|
+
const selected = selectedValue === undefined || selectedValue === null ? '' : String(selectedValue).trim();
|
|
64
|
+
const stored = storedValue === undefined || storedValue === null ? '' : String(storedValue).trim();
|
|
65
|
+
return emptyValues.has(selected.toLowerCase())
|
|
66
|
+
? (emptyValues.has(stored.toLowerCase()) ? '' : stored)
|
|
67
|
+
: selected;
|
|
68
|
+
};
|
|
42
69
|
|
|
43
70
|
const getProfileDefinition = (controllerType) => {
|
|
44
71
|
// The bundle captures a private definition through a RED facade. It does
|
|
@@ -183,10 +210,20 @@
|
|
|
183
210
|
outline: 3px solid #fbbf24;
|
|
184
211
|
outline-offset: 2px;
|
|
185
212
|
}
|
|
186
|
-
#hue-controller-profile-editor.hue-controller-no-knx .hue-knx-section,
|
|
187
213
|
#hue-controller-profile-editor.hue-controller-light-profile.hue-controller-no-knx #tabs {
|
|
188
214
|
display: none !important;
|
|
189
215
|
}
|
|
216
|
+
#hue-controller-profile-editor:not(.hue-controller-light-profile).hue-controller-no-knx .hue-knx-section {
|
|
217
|
+
display: none !important;
|
|
218
|
+
}
|
|
219
|
+
#hue-controller-profile-editor .hue-controller-knx-flow-only-notice {
|
|
220
|
+
box-sizing: border-box;
|
|
221
|
+
padding: 9px 12px;
|
|
222
|
+
margin: 0 0 12px;
|
|
223
|
+
border-left: 4px solid #d79b00;
|
|
224
|
+
background: #fff8df;
|
|
225
|
+
color: #4d3a00;
|
|
226
|
+
}
|
|
190
227
|
/* Every profile inherited slightly different fixed KNX widths. Mark
|
|
191
228
|
actual DPT rows after mounting and compact only their DPT/Name
|
|
192
229
|
controls. No wrapping keeps one mapping on one visual line, while the
|
|
@@ -299,6 +336,16 @@
|
|
|
299
336
|
let selectedHueResource;
|
|
300
337
|
let activeHueServerId;
|
|
301
338
|
let hueDevicePickerMouseDown = false;
|
|
339
|
+
let hueCatalogReady = false;
|
|
340
|
+
let hueReadinessTimer;
|
|
341
|
+
let hueUnreachableTimer;
|
|
342
|
+
let hueReadinessAttempts = 0;
|
|
343
|
+
let hueReadinessErrorShown = false;
|
|
344
|
+
let hueBridgeUnreachable = false;
|
|
345
|
+
let hueEditorClosed = false;
|
|
346
|
+
const HUE_READINESS_INTERVAL_MS = 2000;
|
|
347
|
+
const HUE_READINESS_MAX_ATTEMPTS = 60;
|
|
348
|
+
const HUE_UNREACHABLE_TIMEOUT_MS = 15000;
|
|
302
349
|
const defaultHueDevicePlaceholder = $('#node-input-name').attr('placeholder') || '';
|
|
303
350
|
|
|
304
351
|
const hasLegacyHueNodes = editorContainsLegacyHueNodes();
|
|
@@ -319,6 +366,24 @@
|
|
|
319
366
|
try { return JSON.parse(JSON.stringify(value)); } catch (error) { return value; }
|
|
320
367
|
};
|
|
321
368
|
|
|
369
|
+
// Several profile editors update the live node object while the dialog
|
|
370
|
+
// is open so switching device functions can mount the correct fields.
|
|
371
|
+
// Node-RED cannot roll those direct mutations back by itself on Cancel,
|
|
372
|
+
// therefore retain the complete persisted defaults before editing.
|
|
373
|
+
const originalEditorState = {};
|
|
374
|
+
Object.keys(buildDefaults()).forEach((key) => {
|
|
375
|
+
originalEditorState[key] = {
|
|
376
|
+
present: Object.prototype.hasOwnProperty.call(controllerNode, key),
|
|
377
|
+
value: cloneEditorValue(controllerNode[key])
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
const restoreOriginalEditorState = () => {
|
|
381
|
+
Object.entries(originalEditorState).forEach(([key, snapshot]) => {
|
|
382
|
+
if (snapshot.present) controllerNode[key] = cloneEditorValue(snapshot.value);
|
|
383
|
+
else delete controllerNode[key];
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
322
387
|
const collectCurrentEditor = () => {
|
|
323
388
|
// Collect before unmounting. Exact keys come from the selected private
|
|
324
389
|
// definition, avoiding broad DOM scans and widget-added CSS classes.
|
|
@@ -326,7 +391,13 @@
|
|
|
326
391
|
const definition = getProfileDefinition(controllerNode.hueControllerType);
|
|
327
392
|
const draft = {};
|
|
328
393
|
Object.keys(definition?.defaults || {}).forEach((key) => {
|
|
329
|
-
const
|
|
394
|
+
const fieldValue = readFieldValue($(`#node-input-${key}`), controllerNode[key]);
|
|
395
|
+
let value = fieldValue;
|
|
396
|
+
if (key === 'server') {
|
|
397
|
+
value = resolveSelectedOrStoredSelection(fieldValue, controllerNode.server, KNX_EMPTY_SERVER_VALUES);
|
|
398
|
+
} else if (key === 'serverHue') {
|
|
399
|
+
value = resolveSelectedOrStoredSelection(fieldValue, controllerNode.serverHue, HUE_EMPTY_SERVER_VALUES);
|
|
400
|
+
}
|
|
330
401
|
controllerNode[key] = value;
|
|
331
402
|
legacyContext[key] = value;
|
|
332
403
|
draft[key] = cloneEditorValue(value);
|
|
@@ -342,7 +413,7 @@
|
|
|
342
413
|
// namespaces that belong specifically to this wrapper.
|
|
343
414
|
if (!activeProfile || !legacyContext) return;
|
|
344
415
|
$('#node-input-server').off('.knxUltimateHueControllerLightVisibility');
|
|
345
|
-
$('#node-input-server')
|
|
416
|
+
$(document).off('change.knxUltimateHueControllerKnxVisibility', '#node-input-server');
|
|
346
417
|
const definition = getProfileDefinition(controllerNode.hueControllerType);
|
|
347
418
|
if (definition && typeof definition.oneditcancel === 'function') {
|
|
348
419
|
definition.oneditcancel.call(legacyContext);
|
|
@@ -395,7 +466,11 @@
|
|
|
395
466
|
const getRawHueResourceId = (value) => String(value || '').split('#')[0];
|
|
396
467
|
|
|
397
468
|
const updateSelectedDevicePresentation = (controllerType = controllerNode.hueControllerType) => {
|
|
398
|
-
|
|
469
|
+
// A persisted device remains visible while its Hue Bridge is offline.
|
|
470
|
+
// Catalog readiness controls discovery and editing of the device
|
|
471
|
+
// selection, not whether saved mappings remain usable in the editor.
|
|
472
|
+
const hueServerSelected = resolveHueServerId({ allowStored: true }) !== '';
|
|
473
|
+
const hasDevice = hueServerSelected && getHueDeviceValue() !== '';
|
|
399
474
|
const profile = PROFILE_TYPES[controllerType] || PROFILE_TYPES.light;
|
|
400
475
|
const typeLabel = controllerText(`types.${controllerType}`, profile.label);
|
|
401
476
|
$('#hue-controller-selected-device-type-value').text(typeLabel);
|
|
@@ -405,6 +480,25 @@
|
|
|
405
480
|
$('#node-input-hueControllerType').val(controllerType);
|
|
406
481
|
};
|
|
407
482
|
|
|
483
|
+
const updateHueDeviceControlVisibility = () => {
|
|
484
|
+
const hueServerSelected = resolveHueServerId({ allowStored: true }) !== '';
|
|
485
|
+
const hasPersistedDevice = getHueDeviceValue() !== '';
|
|
486
|
+
const deviceControlVisible = hueServerSelected && (hueCatalogReady || hasPersistedDevice);
|
|
487
|
+
const deviceCatalogPending = hueServerSelected && !hueCatalogReady;
|
|
488
|
+
$('#hue-controller-device-row').toggle(deviceControlVisible);
|
|
489
|
+
$('#node-input-name').prop('disabled', hueServerSelected && !hueCatalogReady);
|
|
490
|
+
$('#hue-controller-devices-loading').toggle(deviceCatalogPending);
|
|
491
|
+
$('#hue-controller-devices-waiting').toggle(!hueBridgeUnreachable);
|
|
492
|
+
$('#hue-controller-devices-unreachable').toggle(hueBridgeUnreachable);
|
|
493
|
+
if (!deviceControlVisible) {
|
|
494
|
+
$('#hue-controller-selected-device-type').hide();
|
|
495
|
+
$('#hue-controller-locate-device').hide();
|
|
496
|
+
$('#hue-controller-profile-editor').hide();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
updateSelectedDevicePresentation();
|
|
500
|
+
};
|
|
501
|
+
|
|
408
502
|
const filterHueResources = (term) => {
|
|
409
503
|
const search = String(term || '').replace(/exactmatch/gi, '').trim().toLowerCase();
|
|
410
504
|
return cachedHueResources.filter((item) => {
|
|
@@ -441,38 +535,114 @@
|
|
|
441
535
|
const reply = typeof response === 'function' ? response : () => { };
|
|
442
536
|
const hueServerId = resolveHueServerId({ allowStored: true });
|
|
443
537
|
if (hueServerId === '') {
|
|
538
|
+
hueCatalogReady = false;
|
|
539
|
+
updateHueDeviceControlVisibility();
|
|
444
540
|
reply([]);
|
|
445
541
|
return;
|
|
446
542
|
}
|
|
447
543
|
if (!forceRefresh && cachedHueResources.length > 0) {
|
|
544
|
+
hueCatalogReady = true;
|
|
545
|
+
updateHueDeviceControlVisibility();
|
|
448
546
|
reply(filterHueResources(term));
|
|
449
547
|
return;
|
|
450
548
|
}
|
|
451
|
-
$('#hue-controller-devices-loading').show();
|
|
452
549
|
const refreshQuery = forceRefresh ? '&forceRefresh=1' : '';
|
|
453
550
|
$.getJSON(`KNXUltimateGetResourcesHUE?rtype=hue_controller&serverId=${encodeURIComponent(hueServerId)}${refreshQuery}&_=${Date.now()}`, (data) => {
|
|
551
|
+
if (hueEditorClosed) return;
|
|
552
|
+
if (resolveHueServerId({ allowStored: true }) !== hueServerId) {
|
|
553
|
+
reply([]);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
454
556
|
const devices = Array.isArray(data) ? data : (Array.isArray(data?.devices) ? data.devices : []);
|
|
455
557
|
cachedHueResources = devices.filter((item) => item && PROFILE_TYPES[item.controllerType] && item.id && item.hueDevice);
|
|
558
|
+
hueCatalogReady = data?.ready === true;
|
|
456
559
|
$('#node-input-name').attr('placeholder', cachedHueResources.length > 0
|
|
457
560
|
? defaultHueDevicePlaceholder
|
|
458
561
|
: controllerText('no_devices', 'No Hue resources found. Check the Hue Bridge and press refresh.'));
|
|
459
|
-
|
|
562
|
+
updateHueDeviceControlVisibility();
|
|
563
|
+
if (hueCatalogReady) reconcileSavedHueResource();
|
|
460
564
|
reply(filterHueResources(term));
|
|
461
565
|
}).fail(() => {
|
|
566
|
+
if (hueEditorClosed) return;
|
|
462
567
|
cachedHueResources = [];
|
|
568
|
+
hueCatalogReady = false;
|
|
569
|
+
updateHueDeviceControlVisibility();
|
|
463
570
|
$('#node-input-name').attr('placeholder', controllerText('no_devices', 'No Hue resources found. Check the Hue Bridge and press refresh.'));
|
|
464
571
|
reply([]);
|
|
465
|
-
}).always(() => {
|
|
466
|
-
$('#hue-controller-devices-loading').hide();
|
|
467
572
|
});
|
|
468
573
|
};
|
|
469
574
|
|
|
575
|
+
const clearHueReadinessTimer = () => {
|
|
576
|
+
if (hueReadinessTimer !== undefined) clearTimeout(hueReadinessTimer);
|
|
577
|
+
hueReadinessTimer = undefined;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const clearHueUnreachableTimer = () => {
|
|
581
|
+
if (hueUnreachableTimer !== undefined) clearTimeout(hueUnreachableTimer);
|
|
582
|
+
hueUnreachableTimer = undefined;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const probeHueBridgeReadiness = ({ reset = false, openPicker = false } = {}) => {
|
|
586
|
+
clearHueReadinessTimer();
|
|
587
|
+
if (reset) {
|
|
588
|
+
hueReadinessAttempts = 0;
|
|
589
|
+
hueReadinessErrorShown = false;
|
|
590
|
+
hueBridgeUnreachable = false;
|
|
591
|
+
clearHueUnreachableTimer();
|
|
592
|
+
}
|
|
593
|
+
const expectedServerId = resolveHueServerId({ allowStored: true });
|
|
594
|
+
if (expectedServerId === '') {
|
|
595
|
+
clearHueUnreachableTimer();
|
|
596
|
+
hueCatalogReady = false;
|
|
597
|
+
updateHueDeviceControlVisibility();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (reset) {
|
|
601
|
+
hueUnreachableTimer = setTimeout(() => {
|
|
602
|
+
if (hueEditorClosed) return;
|
|
603
|
+
if (resolveHueServerId({ allowStored: true }) !== expectedServerId || hueCatalogReady) return;
|
|
604
|
+
hueBridgeUnreachable = true;
|
|
605
|
+
updateHueDeviceControlVisibility();
|
|
606
|
+
}, HUE_UNREACHABLE_TIMEOUT_MS);
|
|
607
|
+
}
|
|
608
|
+
updateHueDeviceControlVisibility();
|
|
609
|
+
hueReadinessAttempts += 1;
|
|
610
|
+
fetchHueResources('', (items) => {
|
|
611
|
+
if (resolveHueServerId({ allowStored: true }) !== expectedServerId) return;
|
|
612
|
+
if (hueCatalogReady) {
|
|
613
|
+
clearHueUnreachableTimer();
|
|
614
|
+
hueBridgeUnreachable = false;
|
|
615
|
+
updateHueDeviceControlVisibility();
|
|
616
|
+
if (openPicker && items.length) $('#node-input-name').autocomplete('search', '');
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (hueReadinessAttempts >= HUE_READINESS_MAX_ATTEMPTS) {
|
|
620
|
+
if (!hueReadinessErrorShown) {
|
|
621
|
+
hueReadinessErrorShown = true;
|
|
622
|
+
updateHueDeviceControlVisibility();
|
|
623
|
+
RED.notify(controllerText(
|
|
624
|
+
'hue_bridge_not_ready',
|
|
625
|
+
'The Hue Bridge did not become ready. Check its connection and reopen the editor.'
|
|
626
|
+
), { type: 'error', fixed: true });
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
hueReadinessTimer = setTimeout(() => {
|
|
631
|
+
probeHueBridgeReadiness({ openPicker });
|
|
632
|
+
}, HUE_READINESS_INTERVAL_MS);
|
|
633
|
+
}, { forceRefresh: true });
|
|
634
|
+
};
|
|
635
|
+
|
|
470
636
|
const configureUnifiedDeviceControl = () => {
|
|
471
637
|
const $deviceName = $('#node-input-name');
|
|
472
638
|
if (!$deviceName.length) return;
|
|
473
639
|
$deviceName.autocomplete({
|
|
474
640
|
minLength: 0,
|
|
475
641
|
source(request, response) {
|
|
642
|
+
if (!hueCatalogReady) {
|
|
643
|
+
response([]);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
476
646
|
fetchHueResources(request.term, response);
|
|
477
647
|
},
|
|
478
648
|
select(event, ui) {
|
|
@@ -615,6 +785,16 @@
|
|
|
615
785
|
// out of the embedded editor makes every device profile more compact.
|
|
616
786
|
$container.find('.form-tips').remove();
|
|
617
787
|
|
|
788
|
+
const $knxFlowOnlyNotice = $container.find('.hue-controller-knx-flow-only-notice');
|
|
789
|
+
const $lightTabs = $container.find('#tabs');
|
|
790
|
+
let knxFlowOnlyNoticeText = 'To display the group-address mappings, select a KNX gateway. Without one, this node can only be controlled by input messages from the flow.';
|
|
791
|
+
try {
|
|
792
|
+
const noticeKey = 'node-red-contrib-knx-ultimate/knxUltimateHueController:knxUltimateHueController.knx_gateway_required_for_mappings';
|
|
793
|
+
const translatedNotice = RED._(noticeKey);
|
|
794
|
+
if (translatedNotice && translatedNotice !== noticeKey) knxFlowOnlyNoticeText = translatedNotice;
|
|
795
|
+
} catch (error) { /* use English fallback */ }
|
|
796
|
+
$knxFlowOnlyNotice.text(knxFlowOnlyNoticeText);
|
|
797
|
+
|
|
618
798
|
const updateControllerKnxVisibility = () => {
|
|
619
799
|
// Node-RED has used several sentinel values for an empty config-node
|
|
620
800
|
// selector. During editor bootstrap the DOM can temporarily expose
|
|
@@ -631,11 +811,39 @@
|
|
|
631
811
|
const hasStoredServer = !KNX_EMPTY_SERVER_VALUES.has(storedServerValue);
|
|
632
812
|
const knxDisabled = !hasDomServer && !hasStoredServer;
|
|
633
813
|
$container.toggleClass('hue-controller-no-knx', knxDisabled);
|
|
814
|
+
$knxFlowOnlyNotice.toggle(controllerType === 'light' && knxDisabled);
|
|
815
|
+
if (controllerType === 'light' && $lightTabs.length) {
|
|
816
|
+
// Device availability controls the outer profile container;
|
|
817
|
+
// KNX selection alone controls the nested mapping tabs.
|
|
818
|
+
const shouldShowLightTabs = !knxDisabled;
|
|
819
|
+
// The mature Light editor also manages this inline style. Make
|
|
820
|
+
// the wrapper authoritative after every lifecycle transition so
|
|
821
|
+
// a transient bootstrap value cannot leave display:none behind
|
|
822
|
+
// after the saved/selected KNX gateway has been restored.
|
|
823
|
+
$lightTabs.css('display', shouldShowLightTabs ? 'flex' : 'none');
|
|
824
|
+
if (shouldShowLightTabs) {
|
|
825
|
+
try { $lightTabs.tabs('refresh'); } catch (error) { /* raw tabs remain visible */ }
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
const handleControllerKnxSelection = function (event) {
|
|
830
|
+
// An explicit user choice must supersede the persisted bootstrap
|
|
831
|
+
// fallback. Node-RED can represent "none" as `_ADD_`, so normalize
|
|
832
|
+
// it before both editors recalculate their visibility.
|
|
833
|
+
const selectedValue = $(this).val();
|
|
834
|
+
if (shouldPreserveStoredSelection(event, selectedValue, controllerNode.server, KNX_EMPTY_SERVER_VALUES)) {
|
|
835
|
+
updateControllerKnxVisibility();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const selectedServer = normalizeKnxServerSelection(selectedValue);
|
|
839
|
+
controllerNode.server = selectedServer;
|
|
840
|
+
if (legacyContext) legacyContext.server = selectedServer;
|
|
841
|
+
updateControllerKnxVisibility();
|
|
634
842
|
};
|
|
635
843
|
updateControllerKnxVisibility();
|
|
636
|
-
$(
|
|
637
|
-
.off('.knxUltimateHueControllerKnxVisibility')
|
|
638
|
-
.on('change.knxUltimateHueControllerKnxVisibility',
|
|
844
|
+
$(document)
|
|
845
|
+
.off('change.knxUltimateHueControllerKnxVisibility', '#node-input-server')
|
|
846
|
+
.on('change.knxUltimateHueControllerKnxVisibility', '#node-input-server', handleControllerKnxSelection);
|
|
639
847
|
updateSelectedDevicePresentation(controllerType);
|
|
640
848
|
focusProfileHelp(controllerType);
|
|
641
849
|
};
|
|
@@ -648,34 +856,55 @@
|
|
|
648
856
|
.off('.knxUltimateHueControllerDevice')
|
|
649
857
|
.on('click.knxUltimateHueControllerDevice', () => {
|
|
650
858
|
cachedHueResources = [];
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}, { forceRefresh: true });
|
|
859
|
+
hueCatalogReady = false;
|
|
860
|
+
probeHueBridgeReadiness({ reset: true, openPicker: true });
|
|
654
861
|
});
|
|
655
|
-
$(
|
|
656
|
-
.off('.
|
|
657
|
-
.on('change.
|
|
658
|
-
const
|
|
862
|
+
$(document)
|
|
863
|
+
.off('change.knxUltimateHueControllerHueServer', '#node-input-serverHue')
|
|
864
|
+
.on('change.knxUltimateHueControllerHueServer', '#node-input-serverHue', (event) => {
|
|
865
|
+
const selectedValue = $('#node-input-serverHue').val();
|
|
866
|
+
if (shouldPreserveStoredSelection(event, selectedValue, controllerNode.serverHue, HUE_EMPTY_SERVER_VALUES)) {
|
|
867
|
+
updateHueDeviceControlVisibility();
|
|
868
|
+
updateSelectedDevicePresentation();
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const nextServerId = normalizeHueServerSelection(selectedValue);
|
|
659
872
|
if (nextServerId === activeHueServerId) return;
|
|
660
873
|
activeHueServerId = nextServerId;
|
|
874
|
+
controllerNode.serverHue = nextServerId;
|
|
875
|
+
if (legacyContext) legacyContext.serverHue = nextServerId;
|
|
876
|
+
clearHueReadinessTimer();
|
|
877
|
+
clearHueUnreachableTimer();
|
|
661
878
|
cachedHueResources = [];
|
|
879
|
+
hueCatalogReady = false;
|
|
880
|
+
hueReadinessErrorShown = false;
|
|
881
|
+
hueBridgeUnreachable = false;
|
|
662
882
|
selectedHueResource = undefined;
|
|
663
883
|
controllerNode.name = '';
|
|
664
884
|
controllerNode.hueDevice = '';
|
|
665
885
|
$('#node-input-name, #node-input-hueDevice').val('');
|
|
886
|
+
updateHueDeviceControlVisibility();
|
|
666
887
|
updateSelectedDevicePresentation();
|
|
888
|
+
if (nextServerId !== '') probeHueBridgeReadiness({ reset: true });
|
|
667
889
|
});
|
|
668
890
|
|
|
669
891
|
this._hueControllerCollectEditor = collectCurrentEditor;
|
|
670
892
|
this._hueControllerGetLegacyContext = () => legacyContext;
|
|
671
893
|
this._hueControllerGetProfile = () => activeProfile;
|
|
672
894
|
this._hueControllerCloseMigration = () => closeMigrationDialog();
|
|
895
|
+
this._hueControllerRestoreOriginalState = restoreOriginalEditorState;
|
|
673
896
|
this._hueControllerCleanupDevice = () => {
|
|
897
|
+
hueEditorClosed = true;
|
|
898
|
+
clearHueReadinessTimer();
|
|
899
|
+
clearHueUnreachableTimer();
|
|
674
900
|
$('#hue-controller-refresh-devices, #node-input-serverHue, #node-input-name').off('.knxUltimateHueControllerDevice');
|
|
901
|
+
$(document).off('change.knxUltimateHueControllerHueServer', '#node-input-serverHue');
|
|
902
|
+
$(document).off('change.knxUltimateHueControllerKnxVisibility', '#node-input-server');
|
|
675
903
|
delete controllerNode.__configureHueControllerDeviceControl;
|
|
676
904
|
};
|
|
677
905
|
renderProfileEditor(selectedType);
|
|
678
|
-
|
|
906
|
+
updateHueDeviceControlVisibility();
|
|
907
|
+
probeHueBridgeReadiness({ reset: true });
|
|
679
908
|
},
|
|
680
909
|
oneditsave() {
|
|
681
910
|
// Preserve lifecycle order: close auxiliary UI, collect mounted fields,
|
|
@@ -705,11 +934,12 @@
|
|
|
705
934
|
delete this._hueControllerGetLegacyContext;
|
|
706
935
|
delete this._hueControllerGetProfile;
|
|
707
936
|
delete this._hueControllerCloseMigration;
|
|
937
|
+
delete this._hueControllerRestoreOriginalState;
|
|
708
938
|
delete this._hueControllerCleanupDevice;
|
|
709
939
|
},
|
|
710
940
|
oneditcancel() {
|
|
711
|
-
// Cancel releases profile-owned editor state
|
|
712
|
-
//
|
|
941
|
+
// Cancel releases profile-owned editor state and restores the snapshot
|
|
942
|
+
// taken before device/profile previews mutated the live node object.
|
|
713
943
|
if (typeof this._hueControllerCloseMigration === 'function') this._hueControllerCloseMigration();
|
|
714
944
|
const profile = typeof this._hueControllerGetProfile === 'function'
|
|
715
945
|
? this._hueControllerGetProfile()
|
|
@@ -722,7 +952,12 @@
|
|
|
722
952
|
definition.oneditcancel.call(legacyContext || this);
|
|
723
953
|
}
|
|
724
954
|
if (typeof this._hueControllerCleanupDevice === 'function') this._hueControllerCleanupDevice();
|
|
955
|
+
if (typeof this._hueControllerRestoreOriginalState === 'function') this._hueControllerRestoreOriginalState();
|
|
956
|
+
delete this._hueControllerCollectEditor;
|
|
957
|
+
delete this._hueControllerGetLegacyContext;
|
|
958
|
+
delete this._hueControllerGetProfile;
|
|
725
959
|
delete this._hueControllerCloseMigration;
|
|
960
|
+
delete this._hueControllerRestoreOriginalState;
|
|
726
961
|
delete this._hueControllerCleanupDevice;
|
|
727
962
|
}
|
|
728
963
|
});
|
|
@@ -759,11 +994,18 @@
|
|
|
759
994
|
</label>
|
|
760
995
|
<input type="text" id="node-input-serverHue">
|
|
761
996
|
</div>
|
|
762
|
-
<
|
|
763
|
-
|
|
764
|
-
<
|
|
765
|
-
|
|
766
|
-
|
|
997
|
+
<div class="form-row" id="hue-controller-devices-loading" style="display:none; color:#666;">
|
|
998
|
+
<label></label>
|
|
999
|
+
<span id="hue-controller-devices-waiting">
|
|
1000
|
+
<i class="fa fa-circle-notch fa-spin" style="color:#1b7d33;"></i>
|
|
1001
|
+
<span data-i18n="knxUltimateHueController.loading_hue_devices"></span>
|
|
1002
|
+
</span>
|
|
1003
|
+
<span id="hue-controller-devices-unreachable" style="display:none; color:#b45309;">
|
|
1004
|
+
<i class="fa fa-exclamation-triangle"></i>
|
|
1005
|
+
<span data-i18n="knxUltimateHueController.hue_bridge_unreachable"></span>
|
|
1006
|
+
</span>
|
|
1007
|
+
</div>
|
|
1008
|
+
<div class="form-row" id="hue-controller-device-row" style="display:none;">
|
|
767
1009
|
<label for="node-input-name">
|
|
768
1010
|
<i class="fa fa-play-circle"></i>
|
|
769
1011
|
<span data-i18n="knxUltimateHueController.hue_device"></span>
|
|
@@ -778,9 +1020,6 @@
|
|
|
778
1020
|
style="display:none; margin-left:6px; color:#ff9800; border-color:#ff9800;" data-i18n="[title]knxUltimateHueController.locate_device">
|
|
779
1021
|
<i class="fa fa-play"></i>
|
|
780
1022
|
</button>
|
|
781
|
-
<span id="hue-controller-devices-loading" style="margin-left:6px; display:none; color:#1b7d33;">
|
|
782
|
-
<i class="fa fa-circle-notch fa-spin"></i>
|
|
783
|
-
</span>
|
|
784
1023
|
<span id="hue-controller-selected-device-type" style="display:none; margin-left:10px; color:#666; white-space:nowrap;">
|
|
785
1024
|
<i class="fa fa-cubes"></i>
|
|
786
1025
|
<span data-i18n="knxUltimateHueController.device_type"></span>:
|
|
@@ -789,7 +1028,6 @@
|
|
|
789
1028
|
<input type="hidden" id="node-input-hueDevice">
|
|
790
1029
|
<input type="hidden" id="node-input-hueControllerType">
|
|
791
1030
|
</div>
|
|
792
|
-
<br />
|
|
793
1031
|
<div id="hue-controller-profile-editor"></div>
|
|
794
1032
|
<br/>
|
|
795
1033
|
<br/>
|
|
@@ -806,6 +1044,12 @@ The unified controller for Hue API v2 resources. Choose a Hue device or resource
|
|
|
806
1044
|
|
|
807
1045
|
Clicking or focusing the Hue device field always opens the complete resource list, even when a device is already selected. Typing continues to filter the list.
|
|
808
1046
|
|
|
1047
|
+
The Hue device picker remains hidden until the selected Hue Bridge is connected and its resource catalog is ready. A loading indicator is shown while the editor waits; selecting `none` hides the indicator and device controls.
|
|
1048
|
+
|
|
1049
|
+
If the node already has a saved Hue device, its picker and KNX mapping tabs remain available while the selected bridge is offline. Resource discovery continues in the background and a fixed red error appears if the bridge does not become ready.
|
|
1050
|
+
|
|
1051
|
+
For Light resources without a KNX gateway, the editor shows a flow-only notice in place of the duplicate “Philips HUE” heading.
|
|
1052
|
+
|
|
809
1053
|
The controller supports lights and grouped lights, plugs, buttons, Tap dial, motion resources, contacts, light level, temperature, humidity, scenes, battery, Zigbee connectivity, and device software update resources.
|
|
810
1054
|
|
|
811
1055
|
## Convert a legacy HUE flow
|
|
@@ -822,7 +1066,11 @@ Each function uses its private mature runtime contract. Hue events remain status
|
|
|
822
1066
|
|
|
823
1067
|
Dedicated legacy HUE nodes remain registered for existing flows, but are frozen and receive no new features or maintenance updates. Node-RED hides their special `deprecated` category from the palette, while existing instances remain editable, deployable, lighter than HUE Controller and visibly marked `(deprecated)` on the canvas. They also show a migration notice at the top of their editor. Before starting, [watch the explanatory migration video on YouTube](https://youtu.be/f0Evf2QFI7c). Use HUE Controller for new work or migrate them with the fully local conversion button; afterwards, the browser opens only an editable usage email draft without navigating away from Node-RED. The email is never sent automatically. Before Deploy, inspect every modified HUE node and verify its function, configuration references, pins and wiring. A fixed Node-RED completion message remains visible until you click **OK** and offers an optional support button; the donation page opens only when that button is clicked.
|
|
824
1068
|
|
|
825
|
-
When no **KNX Gateway** is selected (including **none** and **Add new...**), the
|
|
1069
|
+
When no **KNX Gateway** is selected (including **none** and **Add new...**), only the Light mapping tabs are hidden. Their mounted GA, DPT and Name fields remain unchanged and reappear immediately with the correct vertical tab layout when the gateway is selected again. Hue resource selection and flow-only options remain available.
|
|
1070
|
+
|
|
1071
|
+
Temporary placeholder values emitted programmatically by Node-RED while the KNX and Hue configuration selectors initialize do not replace saved gateways. Only an explicit user selection of **none** hides the Light mapping tabs.
|
|
1072
|
+
|
|
1073
|
+
The Light mapping tabs contain only configuration controls; the decorative Dim, Tunable White and RGB images have been removed.
|
|
826
1074
|
|
|
827
1075
|
For a single light, the **Dim**, **Tunable White**, **RGB/HSV**, and native-effects sections are enabled from the live capabilities reported by the selected Hue API v2 light resource. A light that exposes `dimming`, `color_temperature`, or `color` therefore shows the corresponding KNX mappings automatically.
|
|
828
1076
|
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
<p>Wählen Sie ein Hue-Gerät oder eine Hue-Ressource. HUE Controller erkennt den Gerätetyp automatisch und öffnet den passenden Editor für KNX-Zuordnungen, Verhalten und Node-Anschlüsse.</p>
|
|
5
5
|
<p>Geräteauswahl, Aktualisierung und erkannter Typ stehen wie beim Matter Controller in derselben kompakten Zeile.</p>
|
|
6
6
|
<p>Ein Klick oder Fokus auf das Gerätefeld öffnet immer die vollständige Hue-Ressourcenliste, auch wenn bereits ein Gerät ausgewählt ist. Beim Tippen wird die Liste weiterhin gefiltert.</p>
|
|
7
|
+
<p>Die Auswahl einer anderen Hue-Ressource ist nur eine Vorschau, bis der Editor gespeichert wird. Mit <code>Abbrechen</code> werden das zuvor gespeicherte Gerät, die Funktion und die Zuordnungen wiederhergestellt.</p>
|
|
8
|
+
<p>Die Hue-Geräteauswahl bleibt ausgeblendet, bis eine Hue Bridge ausgewählt und verbunden ist und ihren Ressourcenkatalog geladen hat. Während der Wartezeit wird eine Ladeanzeige angezeigt. Wird die Bridge-Auswahl auf <code>none</code> zurückgesetzt, werden Ladeanzeige, Geräteauswahl und Geräteeditor wieder ausgeblendet.</p>
|
|
9
|
+
<p>Wenn im Knoten bereits ein Hue-Gerät gespeichert ist, bleibt dessen Auswahl sichtbar, aber deaktiviert, während die ausgewählte Bridge offline ist; die KNX-Zuordnungsregister bleiben verfügbar. Nach 15 Sekunden meldet die Warteanzeige, dass die Hue Bridge nicht erreichbar ist. Die Ressourcensuche läuft im Hintergrund weiter; wird die Bridge nicht bereit, erscheint schließlich eine feste rote Fehlermeldung.</p>
|
|
7
10
|
<p>KNX-Zuordnungszeilen halten GA, DPT und Name in einer Zeile. DPT-Auswahl und Namensfeld verwenden kompakte Breiten; das Namensfeld kann sich in schmalen Editoren weiter verkleinern, ohne den gespeicherten Text zu ändern. Gespeicherte DPT-Werte bleiben erhalten, während die Auswahloptionen asynchron geladen werden.</p>
|
|
8
11
|
<p>Unterstützt werden Leuchten und Leuchtengruppen, Steckdosen, Taster, Tap dial, Bewegungs- und Kamerabewegungsressourcen, Kontakte, Lichtstärke, Temperatur, Luftfeuchtigkeit, Szenen, Batterie, Zigbee-Verbindung und Softwareupdate.</p>
|
|
9
|
-
<p>Wenn kein <strong>KNX-Gateway</strong> ausgewählt ist (einschließlich <code>none</code> und <em>Neu hinzufügen...</em>), werden die
|
|
12
|
+
<p>Wenn kein <strong>KNX-Gateway</strong> ausgewählt ist (einschließlich <code>none</code> und <em>Neu hinzufügen...</em>), werden die Registerkarten der Leuchtenzuordnung ausgeblendet; die Auswahl der Hue-Ressource und reine Flow-Optionen bleiben verfügbar. Die zugehörigen Felder GA, DPT und Name bleiben eingebunden und behalten ihre Werte, wenn das Gateway erneut ausgewählt wird; dabei wird auch das vertikale Registerkartenlayout unverändert wiederhergestellt.</p>
|
|
13
|
+
<p>Temporäre Platzhalterwerte, die Node-RED während der Initialisierung der KNX- und Hue-Konfigurationsauswahl programmgesteuert auslöst, ersetzen keine gespeicherten Gateways. Nur die ausdrückliche Benutzerauswahl von <code>none</code> blendet die Leuchtenzuordnungsregister aus.</p>
|
|
14
|
+
<p>Die Registerkarten der Leuchtenzuordnung enthalten nur Konfigurationsfelder; die dekorativen Bilder für Dimmen, abstimmbares Weiß und RGB wurden entfernt.</p>
|
|
10
15
|
<p>Bei einer einzelnen Leuchte richten sich die Bereiche <strong>Dimmen</strong>, <strong>Abstimmbares Weiß</strong>, <strong>RGB/HSV</strong> und native Effekte nach den Live-Fähigkeiten der ausgewählten Hue-API-v2-Ressource. Die zugehörigen KNX-Zuordnungen erscheinen automatisch, wenn die Leuchte <code>dimming</code>, <code>color_temperature</code> oder <code>color</code> bereitstellt.</p>
|
|
11
16
|
<p>Der Leuchteneditor zeigt gespeicherte Zuordnungen sofort an, ohne auf den Hue-Ressourcencache der Runtime zu warten. Aktuelle Fähigkeiten werden im Hintergrund geladen; schlägt die Anfrage fehl, bleiben Zuordnungen und Pin-Auswahl verfügbar und eine feste rote Node-RED-Fehlermeldung meldet den Fehler.</p>
|
|
12
17
|
<p>Locate und der Leuchten-Zuordnungsbereich werden vor den optionalen Effekt- und Register-Widgets initialisiert. Ein gespeichertes KNX-Gateway bleibt gültig, wenn Node-RED beim Start des Editors vorübergehend einen leeren Selektor anzeigt. Browserseitige Fehler und ein abgewiesener erster Locate-Befehl erzeugen eine feste rote Node-RED-Fehlermeldung mit dem technischen Detail, statt den Editor still zu lassen.</p>
|
|
@@ -42,6 +47,8 @@
|
|
|
42
47
|
|
|
43
48
|
<p>Dieser Node steuert HUE-Leuchten (einzeln oder gruppiert) und ordnet Befehle/Zustände KNX-Gruppenadressen zu.</p>
|
|
44
49
|
|
|
50
|
+
<p>Ohne KNX-Gateway erscheint anstelle der doppelten Überschrift „Philips HUE“ ein Hinweis zur reinen Flow-Nutzung.</p>
|
|
51
|
+
|
|
45
52
|
<p><strong>Leuchtengruppen:</strong> wenn ein KNX-Gateway konfiguriert ist, zeigt die Auswahl eines <code>grouped_light</code> immer die vollständigen Zuordnungen für Schalten, Dimmen, Tunable White, RGB/HSV, Effekte und Verhalten. Der Editor schränkt diese Felder nicht anhand der aktuell enthaltenen Leuchten ein.</p>
|
|
46
53
|
|
|
47
54
|
**Allgemein**
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
},
|
|
6
6
|
"knxUltimateHueController": {
|
|
7
7
|
"device_function": "Gerätefunktion",
|
|
8
|
-
"hue_section": "Philips Hue",
|
|
9
8
|
"hue_device": "Hue-Gerät",
|
|
10
9
|
"hue_device_placeholder": "Hue-Gerät oder -Ressource suchen",
|
|
10
|
+
"loading_hue_devices": "Verbindung zur Hue Bridge wird hergestellt und Geräte werden geladen…",
|
|
11
|
+
"hue_bridge_unreachable": "Die Hue Bridge ist nicht erreichbar.",
|
|
11
12
|
"device_type": "Gerätetyp",
|
|
12
13
|
"refresh_devices": "Hue-Geräte aktualisieren",
|
|
13
14
|
"locate_device": "Ausgewählte Hue-Leuchte lokalisieren",
|
|
14
15
|
"no_devices": "Keine Hue-Ressourcen gefunden. Prüfen Sie die Hue Bridge und aktualisieren Sie die Liste.",
|
|
16
|
+
"hue_bridge_not_ready": "Die Hue Bridge wurde nicht bereit. Prüfen Sie die Verbindung und öffnen Sie den Editor erneut.",
|
|
17
|
+
"knx_gateway_required_for_mappings": "Wählen Sie ein KNX-Gateway aus, um die Gruppenadress-Zuordnungen anzuzeigen. Ohne Gateway kann dieser Knoten nur über Eingangsnachrichten aus dem Flow gesteuert werden.",
|
|
15
18
|
"legacy_node_notice": "Dieser Legacy-HUE-Knoten ist veraltet. Verwenden Sie für neue Flows den neuen HUE Controller-Knoten.",
|
|
16
19
|
"migration_button": "Legacy-HUE-Knoten konvertieren",
|
|
17
20
|
"migration_button_title": "Alle Legacy-HUE-Knoten im Editor konvertieren",
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
<p>Select a Hue device or resource. HUE Controller detects its device type automatically and opens the matching KNX mapping, behaviour and node-pin editor.</p>
|
|
5
5
|
<p>The device picker, refresh action and detected type share the same compact row, matching the Matter Controller workflow.</p>
|
|
6
6
|
<p>Clicking or focusing the device field always opens the complete Hue resource list, even when a device is already selected. Typing continues to filter the list.</p>
|
|
7
|
+
<p>Selecting another Hue resource is only a preview until the editor is saved. Pressing <code>Cancel</code> restores the previously saved device, function and mappings.</p>
|
|
8
|
+
<p>The Hue device picker remains hidden until a Hue Bridge is selected, connected, and has loaded its resource catalog. A loading indicator is shown while waiting. Returning the bridge selector to <code>none</code> hides the indicator, picker and device editor again.</p>
|
|
9
|
+
<p>If the node already has a saved Hue device, its picker remains visible but disabled while the selected bridge is offline, and its KNX mapping tabs remain available. After 15 seconds the loading label reports that the Hue Bridge is not reachable. Resource discovery continues in the background and a fixed red error eventually appears if the bridge does not become ready.</p>
|
|
7
10
|
<p>KNX mapping rows keep GA, DPT and Name on one line. DPT selectors and Name fields use compact widths, and the Name field can contract further on narrow editor trays without changing its stored text. Saved DPT values are retained while the selector options load asynchronously.</p>
|
|
8
11
|
<p>Supported functions: lights and grouped lights, plugs, buttons, Tap dial, motion and camera motion, contacts, light level, temperature, humidity, scenes, battery, Zigbee connectivity, and device software update.</p>
|
|
9
|
-
<p>When no <strong>KNX Gateway</strong> is selected (including <code>none</code> and <em>Add new...</em>),
|
|
12
|
+
<p>When no <strong>KNX Gateway</strong> is selected (including <code>none</code> and <em>Add new...</em>), the Light mapping tabs are hidden while Hue resource selection and flow-only options remain available. Their GA, DPT and Name fields stay mounted and retain their values when the gateway is selected again, with the vertical tab layout restored unchanged.</p>
|
|
13
|
+
<p>Temporary placeholder values emitted programmatically by Node-RED while the KNX and Hue configuration selectors initialize do not replace saved gateways. Only an explicit user selection of <code>none</code> hides the Light mapping tabs.</p>
|
|
14
|
+
<p>The Light mapping tabs contain only configuration controls; the decorative Dim, Tunable White and RGB images have been removed.</p>
|
|
10
15
|
<p>For a single light, the <strong>Dim</strong>, <strong>Tunable White</strong>, <strong>RGB/HSV</strong>, and native-effects sections follow the live capabilities reported by the selected Hue API v2 resource. The corresponding KNX mappings appear automatically when the light exposes <code>dimming</code>, <code>color_temperature</code>, or <code>color</code>.</p>
|
|
11
16
|
<p>The Light editor renders saved mappings immediately without waiting for the runtime Hue resource cache. Current capabilities load in the background; if that request fails, the saved mappings and pin selector remain available and a fixed red Node-RED error reports the failure.</p>
|
|
12
17
|
<p>Locate and the Light mapping container initialize before optional Effects and tab widgets. A saved KNX gateway remains valid if Node-RED temporarily exposes an empty selector while the editor starts. Browser-side failures and a rejected initial Locate command produce a fixed red Node-RED error with the technical detail instead of leaving the editor silent.</p>
|
|
@@ -42,6 +47,8 @@
|
|
|
42
47
|
|
|
43
48
|
<p>This node controls Philips Hue lights (single or grouped) and maps their commands/states to KNX.</p>
|
|
44
49
|
|
|
50
|
+
<p>Without a KNX gateway, the editor shows a flow-only notice in place of the duplicate “Philips HUE” heading.</p>
|
|
51
|
+
|
|
45
52
|
<p><strong>Grouped lights:</strong> when a KNX gateway is configured, selecting a <code>grouped_light</code> always displays the complete Switch, Dim, Tunable White, RGB/HSV, Effects and Behaviour mappings. The editor does not restrict these fields according to the group's current child lights.</p>
|
|
46
53
|
|
|
47
54
|
**General**
|