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 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/>
@@ -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 value = readFieldValue($(`#node-input-${key}`), controllerNode[key]);
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').off('.knxUltimateHueControllerKnxVisibility');
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
- const hasDevice = getHueDeviceValue() !== '';
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
- reconcileSavedHueResource();
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
- $('#node-input-server')
637
- .off('.knxUltimateHueControllerKnxVisibility')
638
- .on('change.knxUltimateHueControllerKnxVisibility', updateControllerKnxVisibility);
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
- fetchHueResources('', (items) => {
652
- if (items.length) $('#node-input-name').autocomplete('search', '');
653
- }, { forceRefresh: true });
859
+ hueCatalogReady = false;
860
+ probeHueBridgeReadiness({ reset: true, openPicker: true });
654
861
  });
655
- $('#node-input-serverHue')
656
- .off('.knxUltimateHueControllerDevice')
657
- .on('change.knxUltimateHueControllerDevice', () => {
658
- const nextServerId = resolveHueServerId();
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
- fetchHueResources('', () => { });
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 but deliberately does not
712
- // collect or persist the values currently visible in the dialog.
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
- <br />
763
- <p>
764
- <b><span data-i18n="knxUltimateHueController.hue_section"></span></b>
765
- </p>
766
- <div class="form-row" id="hue-controller-device-row">
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 KNX mapping fields are hidden. Hue resource selection and flow-only options remain available.
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 KNX-Zuordnungsfelder ausgeblendet; die Auswahl der Hue-Ressource und reine Flow-Optionen bleiben verfügbar.</p>
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>), KNX mapping fields are hidden while Hue resource selection and flow-only options remain available.</p>
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**