|
222 | 222 | ensureVerticalTabsStyle(); |
223 | 223 | const $knxServerInput = $("#node-input-server"); |
224 | 224 | const KNX_EMPTY_VALUES = new Set(['', 'none', '_ADD_', '__NONE__']); |
| 225 | + const $hueServerInput = $("#node-input-serverHue"); |
| 226 | + const $hueDeviceInput = $("#node-input-hueDevice"); |
| 227 | + const $deviceNameInput = $("#node-input-name"); |
| 228 | + const $refreshDevicesButton = $(".hue-refresh-devices"); |
| 229 | + const $hueDevicesLoading = $(".hue-devices-loading"); |
| 230 | + let cachedHueDevices = Array.isArray(node._cachedHueLightDevices) ? node._cachedHueLightDevices : []; |
| 231 | + node._cachedHueLightDevices = cachedHueDevices; |
| 232 | + const defaultHueDevicePlaceholder = $deviceNameInput.attr('placeholder') || ''; |
| 233 | + let showingNoHueDevicesPlaceholder = false; |
| 234 | + const HUE_EMPTY_SERVER_VALUES = new Set(['', 'none', '_add_', '__none__', '__null__', 'null', 'undefined']); |
225 | 235 |
|
226 | 236 | const resolveKnxServerValue = () => { |
227 | 237 | const domValue = $knxServerInput.val(); |
|
239 | 249 | if (val === undefined || val === null) return false; |
240 | 250 | return !KNX_EMPTY_VALUES.has(val); |
241 | 251 | }; |
| 252 | + |
| 253 | + const resolveHueServerValue = () => { |
| 254 | + const domValue = $hueServerInput.val(); |
| 255 | + if (domValue !== undefined && domValue !== null) { |
| 256 | + const trimmed = String(domValue).trim(); |
| 257 | + if (trimmed !== '' && !HUE_EMPTY_SERVER_VALUES.has(trimmed.toLowerCase())) return trimmed; |
| 258 | + } |
| 259 | + if (node.serverHue !== undefined && node.serverHue !== null) { |
| 260 | + const stored = String(node.serverHue).trim(); |
| 261 | + if (stored !== '' && !HUE_EMPTY_SERVER_VALUES.has(stored.toLowerCase())) return stored; |
| 262 | + } |
| 263 | + return ''; |
| 264 | + }; |
| 265 | + |
| 266 | + const getHueServerConfig = () => { |
| 267 | + const id = resolveHueServerValue(); |
| 268 | + if (id === '') return null; |
| 269 | + try { |
| 270 | + return RED.nodes.node(id) || null; |
| 271 | + } catch (error) { |
| 272 | + return null; |
| 273 | + } |
| 274 | + }; |
| 275 | + |
| 276 | + const resolveDeviceTypeSuffix = (deviceType) => { |
| 277 | + const normalized = (deviceType || '').toLowerCase(); |
| 278 | + return normalized === 'grouped_light' ? '#grouped_light' : '#light'; |
| 279 | + }; |
| 280 | + |
| 281 | + const applyHueDevicesPlaceholder = (hasDevices) => { |
| 282 | + if (!$deviceNameInput.length) return; |
| 283 | + if (hasDevices) { |
| 284 | + if (showingNoHueDevicesPlaceholder) { |
| 285 | + showingNoHueDevicesPlaceholder = false; |
| 286 | + $deviceNameInput.attr('placeholder', defaultHueDevicePlaceholder); |
| 287 | + } |
| 288 | + return; |
| 289 | + } |
| 290 | + const message = node._('knxUltimateHueLight.no_devices') || defaultHueDevicePlaceholder; |
| 291 | + showingNoHueDevicesPlaceholder = true; |
| 292 | + $deviceNameInput.attr('placeholder', message); |
| 293 | + if (($deviceNameInput.val() || '').trim() === '') { |
| 294 | + $deviceNameInput.val(''); |
| 295 | + } |
| 296 | + }; |
| 297 | + |
| 298 | + const filterHueDevices = (devices, term) => { |
| 299 | + const cleaned = (term || '').replace(/exactmatch/gi, '').trim(); |
| 300 | + return $.map(devices, (value) => { |
| 301 | + if (!value || !value.id) return null; |
| 302 | + const deviceName = value.name || value.id; |
| 303 | + if (cleaned !== '' && !htmlUtilsfullCSVSearch(deviceName, cleaned)) { |
| 304 | + return null; |
| 305 | + } |
| 306 | + const suffix = resolveDeviceTypeSuffix(value.deviceType); |
| 307 | + return { |
| 308 | + hueDevice: `${value.id}${suffix}`, |
| 309 | + value: deviceName, |
| 310 | + deviceObject: value.deviceObject || value, |
| 311 | + deviceType: value.deviceType || 'light' |
| 312 | + }; |
| 313 | + }); |
| 314 | + }; |
| 315 | + |
| 316 | + const fetchHueDevices = (term, respond, { forceRefresh = false } = {}) => { |
| 317 | + const hueServer = getHueServerConfig(); |
| 318 | + if (!hueServer) { |
| 319 | + applyHueDevicesPlaceholder(true); |
| 320 | + if (typeof respond === 'function') respond([]); |
| 321 | + return; |
| 322 | + } |
| 323 | + if (!forceRefresh && Array.isArray(cachedHueDevices) && cachedHueDevices.length > 0) { |
| 324 | + applyHueDevicesPlaceholder(cachedHueDevices.length > 0); |
| 325 | + if (typeof respond === 'function') respond(filterHueDevices(cachedHueDevices, term)); |
| 326 | + return; |
| 327 | + } |
| 328 | + if ($hueDevicesLoading.length) { |
| 329 | + $hueDevicesLoading.show(); |
| 330 | + } |
| 331 | + $.getJSON(`KNXUltimateGetResourcesHUE?rtype=light&serverId=${encodeURIComponent(hueServer.id)}&_=${Date.now()}`, (data) => { |
| 332 | + const listCandidates = Array.isArray(data) ? data : (Array.isArray(data?.devices) ? data.devices : []); |
| 333 | + cachedHueDevices = listCandidates.map((value) => { |
| 334 | + const deviceObject = value.deviceObject || value; |
| 335 | + const rawId = deviceObject?.id || value.id || value.rid || ''; |
| 336 | + const trimmedId = typeof rawId === 'string' ? rawId.trim() : rawId; |
| 337 | + if (trimmedId === undefined || trimmedId === null || String(trimmedId).trim() === '') return null; |
| 338 | + return { |
| 339 | + id: String(trimmedId).trim(), |
| 340 | + name: value.name || value.metadata?.name || deviceObject?.metadata?.name || '', |
| 341 | + deviceType: deviceObject?.type || value.type || '', |
| 342 | + deviceObject |
| 343 | + }; |
| 344 | + }).filter(Boolean); |
| 345 | + node._cachedHueLightDevices = cachedHueDevices; |
| 346 | + applyHueDevicesPlaceholder(cachedHueDevices.length > 0); |
| 347 | + if (typeof respond === 'function') respond(filterHueDevices(cachedHueDevices, term)); |
| 348 | + }).always(() => { |
| 349 | + if ($hueDevicesLoading.length) { |
| 350 | + $hueDevicesLoading.hide(); |
| 351 | + } |
| 352 | + }).fail(() => { |
| 353 | + cachedHueDevices = []; |
| 354 | + node._cachedHueLightDevices = cachedHueDevices; |
| 355 | + applyHueDevicesPlaceholder(false); |
| 356 | + if ($hueDevicesLoading.length) { |
| 357 | + $hueDevicesLoading.hide(); |
| 358 | + } |
| 359 | + if (typeof respond === 'function') respond([]); |
| 360 | + }); |
| 361 | + }; |
| 362 | + |
242 | 363 | // TIMER BLINK #################################################### |
243 | 364 | let blinkStatus = 2; |
244 | 365 | let timerBlinkBackground; |
|
437 | 558 | const $pinSectionRow = $("#node-input-enableNodePINS").closest('.form-row'); |
438 | 559 | const $pinSelect = $("#node-input-enableNodePINS"); |
439 | 560 | const $pinInfoRow = $pinSectionRow.next('.form-tips'); |
440 | | - const $hueDeviceInput = $("#node-input-hueDevice"); |
441 | 561 | $tabs.addClass('hue-vertical-tabs'); |
442 | 562 | $tabs.tabs(); // Tabs gestione KNX |
443 | 563 | $tabs.find('ul').addClass('ui-tabs-nav'); |
|
655 | 775 |
|
656 | 776 | $hueDeviceInput.on('change.knxUltimateHueLight input.knxUltimateHueLight', updateTabsVisibility); |
657 | 777 |
|
| 778 | + if (($deviceNameInput.val() || '').trim() !== '') { |
| 779 | + applyHueDevicesPlaceholder(true); |
| 780 | + } else { |
| 781 | + applyHueDevicesPlaceholder(cachedHueDevices.length > 0); |
| 782 | + } |
| 783 | + |
| 784 | + if ($deviceNameInput.length) { |
| 785 | + if ($deviceNameInput.data('ui-autocomplete')) { |
| 786 | + try { $deviceNameInput.autocomplete('destroy'); } catch (error) { /* empty */ } |
| 787 | + } |
| 788 | + $deviceNameInput.autocomplete({ |
| 789 | + minLength: 0, |
| 790 | + source(request, response) { |
| 791 | + fetchHueDevices(request.term, response); |
| 792 | + }, |
| 793 | + select(event, ui) { |
| 794 | + if (!ui.item || !ui.item.hueDevice || ui.item.hueDevice === 'error') { |
| 795 | + event.preventDefault(); |
| 796 | + return; |
| 797 | + } |
| 798 | + $deviceNameInput.val(ui.item.value || ''); |
| 799 | + node.name = ui.item.value || node.name; |
| 800 | + $hueDeviceInput.val(ui.item.hueDevice); |
| 801 | + node.hueDevice = ui.item.hueDevice; |
| 802 | + node.hueDeviceObject = ui.item.deviceObject || { type: ui.item.deviceType }; |
| 803 | + updateTabsVisibility(); |
| 804 | + setTimeout(() => { |
| 805 | + try { $deviceNameInput.autocomplete('close'); } catch (error) { /* empty */ } |
| 806 | + Go(); |
| 807 | + }, 0); |
| 808 | + }, |
| 809 | + focus(event, ui) { |
| 810 | + event.preventDefault(); |
| 811 | + $deviceNameInput.val(ui.item && ui.item.value ? ui.item.value : ''); |
| 812 | + } |
| 813 | + }).focus(function () { |
| 814 | + $(this).autocomplete('search', `${$(this).val()}exactmatch`); |
| 815 | + }).on('input.knxUltimateHueLight', function () { |
| 816 | + if ($(this).val().trim() === '') { |
| 817 | + $hueDeviceInput.val(''); |
| 818 | + node.hueDevice = ''; |
| 819 | + updateTabsVisibility(); |
| 820 | + } |
| 821 | + }); |
| 822 | + } |
| 823 | + |
| 824 | + if ($refreshDevicesButton.length) { |
| 825 | + $refreshDevicesButton.off('.knxUltimateHueLight').on('click.knxUltimateHueLight', () => { |
| 826 | + cachedHueDevices = []; |
| 827 | + node._cachedHueLightDevices = cachedHueDevices; |
| 828 | + fetchHueDevices('', () => { |
| 829 | + if ($deviceNameInput.length) { |
| 830 | + $deviceNameInput.autocomplete('search', `${$deviceNameInput.val()}exactmatch`); |
| 831 | + } |
| 832 | + }, { forceRefresh: true }); |
| 833 | + }); |
| 834 | + } |
| 835 | + |
| 836 | + if ($hueServerInput.length) { |
| 837 | + $hueServerInput.off('.knxUltimateHueLightDevices').on('change.knxUltimateHueLightDevices', () => { |
| 838 | + cachedHueDevices = []; |
| 839 | + node._cachedHueLightDevices = cachedHueDevices; |
| 840 | + showingNoHueDevicesPlaceholder = false; |
| 841 | + applyHueDevicesPlaceholder(true); |
| 842 | + if ($hueDevicesLoading.length) { |
| 843 | + $hueDevicesLoading.hide(); |
| 844 | + } |
| 845 | + }); |
| 846 | + } |
| 847 | + |
658 | 848 | // Get the HUE capabilities to enable/disable UI parts |
659 | 849 | var getJsonPromise; |
660 | 850 | const initialHueDeviceRaw = getHueDeviceValue(); |
|
1038 | 1228 | } catch (error) { } |
1039 | 1229 | }); |
1040 | 1230 |
|
1041 | | - // Autocomplete suggestion with HUE Lights |
1042 | | - $("#node-input-name").autocomplete({ |
1043 | | - minLength: 0, |
1044 | | - source: function (request, response) { |
1045 | | - $.getJSON("KNXUltimateGetResourcesHUE?rtype=light&serverId=" + $("#node-input-serverHue").val() + "&" + { _: new Date().getTime() }, (data) => { |
1046 | | - response( |
1047 | | - $.map(data.devices, function (value, key) { |
1048 | | - var sSearch = value.name; |
1049 | | - if (!value.name.includes("I'm still connecting")) { |
1050 | | - if (htmlUtilsfullCSVSearch(sSearch, request.term)) { |
1051 | | - return { |
1052 | | - hueDevice: value.id, |
1053 | | - value: value.name, |
1054 | | - }; |
1055 | | - } else { |
1056 | | - return null; |
1057 | | - } |
1058 | | - } else { |
1059 | | - return { |
1060 | | - hueDevice: value.id, |
1061 | | - value: value.name, |
1062 | | - }; |
1063 | | - } |
1064 | | - }) |
1065 | | - ); |
1066 | | - }); |
1067 | | - }, |
1068 | | - select: function (event, ui) { |
1069 | | - // Distinguish between group of lights an single light. |
1070 | | - const $nameInput = $("#node-input-name"); |
1071 | | - $nameInput.val(ui.item.value || ''); |
1072 | | - node.name = ui.item.value || node.name; |
1073 | | - if (ui.item.value.toLowerCase().startsWith("grouped_light")) { |
1074 | | - const storedValue = ui.item.hueDevice + "#grouped_light"; |
1075 | | - $("#node-input-hueDevice").val(storedValue); |
1076 | | - node.hueDevice = storedValue; |
1077 | | - } else { |
1078 | | - const storedValue = ui.item.hueDevice + "#light"; |
1079 | | - $("#node-input-hueDevice").val(storedValue); |
1080 | | - node.hueDevice = storedValue; |
1081 | | - } |
1082 | | - updateTabsVisibility(); |
1083 | | - Go(); |
1084 | | - } |
1085 | | - }).focus(function () { |
1086 | | - $(this).autocomplete('search', $(this).val() + 'exactmatch'); |
1087 | | - }); |
1088 | | - |
1089 | 1231 | // Timer connection to backend #################################################### |
1090 | 1232 | let timerWaitBackEndCounter = 0; |
1091 | 1233 | function checkConnection() { |
|
1174 | 1316 | const serialized = JSON.stringify(collectedEffectRules); |
1175 | 1317 | $("#node-input-effectRules").val(serialized); |
1176 | 1318 | this.effectRules = serialized; |
| 1319 | + this._cachedHueLightDevices = []; |
1177 | 1320 | }, |
1178 | 1321 | oneditcancel: function () { |
1179 | 1322 | // Return to the info tab |
|
1183 | 1326 | //RED.sidebar.removeTab("tabNRColor"); |
1184 | 1327 | //RED.sidebar.show("help"); |
1185 | 1328 |
|
| 1329 | + this._cachedHueLightDevices = []; |
1186 | 1330 | }, |
1187 | 1331 | oneditresize: function (size) { |
1188 | 1332 | //var height = size.height; |
|
1307 | 1451 | </p> |
1308 | 1452 |
|
1309 | 1453 | <div class="form-row"> |
1310 | | - <label for="node-input-hueDevice"> |
1311 | | - <i class="fa fa-play-circle"></i> <span data-i18n="common.name"></span></label> |
1312 | | - <input type="text" id="node-input-name" placeholder="Enter your hue device name" /> |
| 1454 | + <label for="node-input-name"> |
| 1455 | + <i class="fa fa-play-circle"></i> <span data-i18n="common.name"></span> |
| 1456 | + </label> |
| 1457 | + <input type="text" id="node-input-name" placeholder="Enter your hue device name" |
| 1458 | + style="flex:1 1 240px; min-width:240px; max-width:240px;" /> |
| 1459 | + <button type="button" class="red-ui-button hue-refresh-devices" |
| 1460 | + style="margin-left:6px; color:#1b7d33; border-color:#1b7d33;"> |
| 1461 | + <i class="fa fa-sync"></i> |
| 1462 | + </button> |
| 1463 | + <span class="hue-devices-loading" style="margin-left:6px; display:none; color:#1b7d33;"> |
| 1464 | + <i class="fa fa-circle-notch fa-spin"></i> |
| 1465 | + </span> |
1313 | 1466 | <input type="hidden" id="node-input-hueDevice" /> |
1314 | 1467 | </div> |
1315 | 1468 |
|
|
0 commit comments