UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, and KNX routing between interfaces. Easy to use and highly configurable.

1,111 lines (1,000 loc) 120 kB
'use strict' <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script> <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script> <script type="text/javascript"> (function () { RED.nodes.registerType("knxUltimateHueLight", { category: "KNX Ultimate HUE", color: "#C0C7E9", defaults: { //buttonState: {value: true}, server: { type: "knxUltimate-config", required: false }, serverHue: { type: "hue-config", required: true }, name: { value: "" }, nameLightSwitch: { value: "" }, GALightSwitch: { value: "" }, dptLightSwitch: { value: "" }, nameLightState: { value: "" }, GALightState: { value: "" }, dptLightState: { value: "" }, nameLightDIM: { value: "" }, GALightDIM: { value: "" }, dptLightDIM: { value: "" }, // TAB Color --------------------------- nameLightColor: { value: "" }, GALightColor: { value: "" }, dptLightColor: { value: "" }, nameLightColorState: { value: "" }, GALightColorState: { value: "" }, dptLightColorState: { value: "" }, // HSV H hue Color change nameLightHSV_H_DIM: { value: "" }, GALightHSV_H_DIM: { value: "" }, dptLightHSV_H_DIM: { value: "" }, nameLightHSV_H_State: { value: "" }, GALightHSV_H_State: { value: "" }, dptLightHSV_H_State: { value: "" }, // HSV S saturation change nameLightHSV_S_DIM: { value: "" }, GALightHSV_S_DIM: { value: "" }, dptLightHSV_S_DIM: { value: "" }, nameLightHSV_S_State: { value: "" }, GALightHSV_S_State: { value: "" }, dptLightHSV_S_State: { value: "" }, // ------------------------------------- nameLightKelvinDIM: { value: "" }, GALightKelvinDIM: { value: "" }, dptLightKelvinDIM: { value: "" }, nameLightKelvinPercentage: { value: "" }, GALightKelvinPercentage: { value: "" }, dptLightKelvinPercentage: { value: "" }, nameLightKelvinPercentageState: { value: "" }, GALightKelvinPercentageState: { value: "" }, dptLightKelvinPercentageState: { value: "" }, nameLightKelvin: { value: "" }, GALightKelvin: { value: "" }, dptLightKelvin: { value: "" }, nameLightKelvinState: { value: "" }, GALightKelvinState: { value: "" }, dptLightKelvinState: { value: "" }, nameLightBrightness: { value: "" }, GALightBrightness: { value: "" }, dptLightBrightness: { value: "" }, nameLightBrightnessState: { value: "" }, GALightBrightnessState: { value: "" }, dptLightBrightnessState: { value: "" }, nameLightBlink: { value: "" }, GALightBlink: { value: "" }, dptLightBlink: { value: "" }, nameLightColorCycle: { value: "" }, GALightColorCycle: { value: "" }, dptLightColorCycle: { value: "" }, nameLightEffect: { value: "" }, GALightEffect: { value: "" }, dptLightEffect: { value: "" }, nameLightEffectStatus: { value: "" }, GALightEffectStatus: { value: "" }, dptLightEffectStatus: { value: "" }, effectRules: { value: '[]' }, nameDaylightSensor: { value: "" }, GADaylightSensor: { value: "" }, dptDaylightSensor: { value: "" }, specifySwitchOnBrightness: { value: "temperature" }, colorAtSwitchOnDayTime: { value: '{"kelvin":3000, "brightness":100 }' }, enableDayNightLighting: { value: "no" }, colorAtSwitchOnNightTime: { value: '{ "kelvin":2700, "brightness":20 }' }, invertDayNight: { value: false }, invertDimTunableWhiteDirection: { value: false }, updateKNXBrightnessStatusOnHUEOnOff: { value: "no" }, dimSpeed: { value: 5000, required: false }, HSVDimSpeed: { value: 5000, required: false }, minDimLevelLight: { value: 10, required: false }, maxDimLevelLight: { value: 100, required: false }, readStatusAtStartup: { value: "yes" }, enableNodePINS: { value: "no" }, outputs: { value: 0 }, inputs: { value: 0 }, hueDevice: { value: "" }, hueDeviceObject: { value: {} }, restoreDayMode: { value: "no" }, // Starting from v 4.1.31 updateLocalStateFromKNXWrite: { value: true } // Starting from v 4.1.31 }, inputs: 0, outputs: 0, icon: "node-hue-icon.svg", label: function () { return this.name || "Hue Light/Outlet"; }, paletteLabel: "Hue Light/Outlet", oneditprepare: function () { // Go to the help panel try { RED.sidebar.show("help"); } catch (error) { } var node = this; // Starting from v 4.1.31 $("#node-input-updateLocalStateFromKNXWrite").prop("checked", node.updateLocalStateFromKNXWrite === true || node.updateLocalStateFromKNXWrite === "true"); // Starting from v 4.1.31 const ensureConfigSelection = (selector) => { if ($(selector).val() !== "_ADD_") return; try { $(selector).prop("selectedIndex", 0); } catch (error) { // Ignore UI quirks for legacy Node-RED versions } }; ["#node-input-serverHue"].forEach(ensureConfigSelection); function ensureVerticalTabsStyle() { if ($('#knxUltimateHueLightVerticalTabs').length) return; const style = ` <style id="knxUltimateHueLightVerticalTabs"> .hue-vertical-tabs.ui-tabs.ui-widget.ui-widget-content.ui-corner-all { display: flex; border: none; padding: 0; } .hue-vertical-tabs > ul.ui-tabs-nav { flex: 0 0 180px; border-right: 1px solid #ccc; border-left: none; border-top: none; border-bottom: none; padding: 0.5em 0.3em; } .hue-vertical-tabs > ul.ui-tabs-nav li { float: none; width: 100%; margin: 0 0 2px 0; } .hue-vertical-tabs > ul.ui-tabs-nav li a { display: block; width: 100%; white-space: normal; position: relative; border-bottom: none !important; } .hue-vertical-tabs > ul.ui-tabs-nav li.ui-tabs-active { border-bottom: none !important; } .hue-vertical-tabs > ul.ui-tabs-nav li.ui-tabs-active a::after { content: ""; position: absolute; left: 0; bottom: 0; width: 50%; height: 3px; background: currentColor; } .hue-vertical-tabs .ui-tabs-panel { flex: 1; padding: 0.8em 1em; box-sizing: border-box; border: none; background: transparent; } .hue-vertical-tabs .form-row > dt { flex: 1 1 auto; margin: 0; } .hue-vertical-tabs hr { width: 100%; border: 0; border-top: 1px solid #ccc; margin: 8px 0; } </style>`; $('head').append(style); } function onEditPrepare() { ensureVerticalTabsStyle(); const $knxServerInput = $("#node-input-server"); const KNX_EMPTY_VALUES = new Set(['', 'none', '_ADD_', '__NONE__']); const $hueServerInput = $("#node-input-serverHue"); const $hueDeviceInput = $("#node-input-hueDevice"); const $deviceNameInput = $("#node-input-name"); const $refreshDevicesButton = $(".hue-refresh-devices"); const $locateDeviceButton = $(".hue-locate-device"); const $hueDevicesLoading = $(".hue-devices-loading"); let cachedHueDevices = Array.isArray(node._cachedHueLightDevices) ? node._cachedHueLightDevices : []; node._cachedHueLightDevices = cachedHueDevices; const defaultHueDevicePlaceholder = $deviceNameInput.attr('placeholder') || ''; let showingNoHueDevicesPlaceholder = false; const HUE_EMPTY_SERVER_VALUES = new Set(['', 'none', '_add_', '__none__', '__null__', 'null', 'undefined']); let locateSessionActive = false; let locateAutoResetTimer = null; let locatePendingRequest = null; node.__stopHueLocateSession = null; node.__cleanupNodeRemovalListener = null; if (!node.__locateSessionInfo) node.__locateSessionInfo = null; node.__hueLocateActive = false; const clearLocateAutoReset = () => { if (locateAutoResetTimer !== null) { clearTimeout(locateAutoResetTimer); locateAutoResetTimer = null; } }; const scheduleLocateAutoReset = (durationMs) => { clearLocateAutoReset(); const ms = Number(durationMs); if (!Number.isFinite(ms) || ms <= 0) return; locateAutoResetTimer = setTimeout(() => { updateLocateButtonState(false); }, ms); }; const updateLocateButtonState = (isActive) => { locateSessionActive = !!isActive; node.__hueLocateActive = locateSessionActive; if (!locateSessionActive) { clearLocateAutoReset(); node.__locateSessionInfo = null; } if (!$locateDeviceButton.length) return; const $icon = $locateDeviceButton.find('i').first(); if ($icon.length) { $icon.removeClass('fa-stop fa-play').addClass(locateSessionActive ? 'fa fa-stop' : 'fa fa-play'); } const title = locateSessionActive ? (node._('knxUltimateHueLight.locate_stop_title') || 'Stop Hue locate') : (node._('knxUltimateHueLight.locate_start_title') || 'Locate selected Hue device'); $locateDeviceButton.attr('title', title); }; const removeNodeRemovalListener = () => { if (typeof node.__cleanupNodeRemovalListener === 'function') { try { RED.events.removeListener('nodes:remove', node.__cleanupNodeRemovalListener); } catch (error) { /* empty */ } node.__cleanupNodeRemovalListener = null; } }; const collectRemovedIds = (input, bucket = new Set()) => { if (!input) return bucket; if (Array.isArray(input)) { input.forEach((entry) => collectRemovedIds(entry, bucket)); return bucket; } if (typeof input === 'string') { bucket.add(input); return bucket; } if (typeof input === 'object') { if (input.id) bucket.add(input.id); if (input.nodes) collectRemovedIds(input.nodes, bucket); } return bucket; }; const handleNodeRemoved = (payload) => { const ids = collectRemovedIds(payload); if (ids.has(node.id) && typeof node.__stopHueLocateSession === 'function') { node.__stopHueLocateSession(); removeNodeRemovalListener(); } }; RED.events.on('nodes:remove', handleNodeRemoved); node.__cleanupNodeRemovalListener = handleNodeRemoved; const buildLocateContext = ({ allowStored = false } = {}) => { const hueServerId = resolveHueServerValue({ allowStored }); const rawDevice = getHueDeviceValue({ allowStored }); if (!hueServerId || !rawDevice) return null; const parts = String(rawDevice).split('#'); const deviceId = (parts[0] || '').trim(); if (deviceId === '') return null; const deviceType = (parts[1] || 'light').trim() || 'light'; return { serverId: hueServerId, deviceId, deviceType }; }; const performLocateRequest = ({ silent = false, allowStoredContext = false, action } = {}) => { const context = buildLocateContext({ allowStored: allowStoredContext }); if (!context) { if (!silent) { const message = !resolveHueServerValue({ allowStored: true }) ? (node._('knxUltimateHueLight.locate_no_bridge') || 'Select a Hue bridge first') : (node._('knxUltimateHueLight.locate_no_device') || 'Select a Hue device first'); RED.notify(message, 'warning'); } updateLocateButtonState(false); return null; } if ($locateDeviceButton.length) { $locateDeviceButton.prop('disabled', true); } const effectiveAction = action || (locateSessionActive ? 'stop' : 'start'); const request = $.ajax({ type: 'POST', url: 'KNXUltimateLocateHueDevice', data: { serverId: context.serverId, deviceId: context.deviceId, deviceType: context.deviceType, action: effectiveAction } }); locatePendingRequest = request; if (!allowStoredContext && effectiveAction === 'start') { node.__locateSessionInfo = { serverId: context.serverId, deviceId: context.deviceId, deviceType: context.deviceType }; } else if (!node.__locateSessionInfo && effectiveAction !== 'stop') { node.__locateSessionInfo = { serverId: context.serverId, deviceId: context.deviceId, deviceType: context.deviceType }; } request.done((response) => { const statusValue = (response && typeof response.status === 'string') ? response.status.toLowerCase() : 'started'; let messageKey; if (effectiveAction === 'stop' || statusValue === 'stopped') { messageKey = 'knxUltimateHueLight.locate_stopped'; node.__locateSessionInfo = null; updateLocateButtonState(false); clearLocateAutoReset(); } else if (effectiveAction === 'start' || statusValue === 'started') { messageKey = 'knxUltimateHueLight.locate_started'; node.__locateSessionInfo = { serverId: context.serverId, deviceId: context.deviceId, deviceType: context.deviceType }; updateLocateButtonState(true); scheduleLocateAutoReset(response && typeof response.expiresInMs === 'number' ? response.expiresInMs : 600000); } else { messageKey = 'knxUltimateHueLight.locate_success'; node.__locateSessionInfo = null; updateLocateButtonState(false); clearLocateAutoReset(); } if (!silent) { const message = node._(messageKey) || (statusValue === 'stopped' ? 'Locate stopped' : 'Locate command sent'); RED.notify(message, 'success'); } }).fail((xhr) => { let message; if (xhr && xhr.responseJSON && xhr.responseJSON.error) { message = xhr.responseJSON.error; } updateLocateButtonState(false); clearLocateAutoReset(); if (!silent) { RED.notify(message || (node._('knxUltimateHueLight.locate_error') || 'Unable to locate Hue device'), 'error'); } }).always(() => { locatePendingRequest = null; if ($locateDeviceButton.length) { $locateDeviceButton.prop('disabled', false); } }); return request; }; const resolveKnxServerValue = () => { const domValue = $knxServerInput.val(); if (domValue !== undefined && domValue !== null && domValue !== '') { return domValue; } if (node.server !== undefined && node.server !== null) { return node.server; } return ''; }; const hasKnxServerSelected = () => { const val = resolveKnxServerValue(); if (val === undefined || val === null) return false; return !KNX_EMPTY_VALUES.has(val); }; const resolveHueServerValue = ({ allowStored = false } = {}) => { if ($hueServerInput.length) { const domValue = $hueServerInput.val(); if (domValue !== undefined && domValue !== null) { const trimmed = String(domValue).trim(); if (trimmed !== '' && !HUE_EMPTY_SERVER_VALUES.has(trimmed.toLowerCase())) return trimmed; } } if (node.serverHue !== undefined && node.serverHue !== null) { const stored = String(node.serverHue).trim(); if (stored !== '' && !HUE_EMPTY_SERVER_VALUES.has(stored.toLowerCase())) return stored; } if (allowStored && node.__locateSessionInfo && node.__locateSessionInfo.serverId) { return node.__locateSessionInfo.serverId; } return ''; }; const getHueServerConfig = () => { const id = resolveHueServerValue(); if (id === '') return null; try { return RED.nodes.node(id) || null; } catch (error) { return null; } }; const resolveDeviceTypeSuffix = (deviceType) => { const normalized = (deviceType || '').toLowerCase(); return normalized === 'grouped_light' ? '#grouped_light' : '#light'; }; const applyHueDevicesPlaceholder = (hasDevices) => { if (!$deviceNameInput.length) return; if (hasDevices) { if (showingNoHueDevicesPlaceholder) { showingNoHueDevicesPlaceholder = false; $deviceNameInput.attr('placeholder', defaultHueDevicePlaceholder); } return; } const message = node._('knxUltimateHueLight.no_devices') || defaultHueDevicePlaceholder; showingNoHueDevicesPlaceholder = true; $deviceNameInput.attr('placeholder', message); if (($deviceNameInput.val() || '').trim() === '') { $deviceNameInput.val(''); } }; const filterHueDevices = (devices, term) => { const cleaned = (term || '').replace(/exactmatch/gi, '').trim(); return $.map(devices, (value) => { if (!value || !value.id) return null; const deviceName = value.name || value.id; if (cleaned !== '' && !htmlUtilsfullCSVSearch(deviceName, cleaned)) { return null; } const suffix = resolveDeviceTypeSuffix(value.deviceType); return { hueDevice: `${value.id}${suffix}`, value: deviceName, deviceObject: value.deviceObject || value, deviceType: value.deviceType || 'light' }; }); }; const fetchHueDevices = (term, respond, { forceRefresh = false } = {}) => { const hueServer = getHueServerConfig(); if (!hueServer) { applyHueDevicesPlaceholder(true); if (typeof respond === 'function') respond([]); node.__locateSessionInfo = null; updateLocateButtonState(false); return; } if (!forceRefresh && Array.isArray(cachedHueDevices) && cachedHueDevices.length > 0) { applyHueDevicesPlaceholder(cachedHueDevices.length > 0); if (typeof respond === 'function') respond(filterHueDevices(cachedHueDevices, term)); return; } if ($hueDevicesLoading.length) { $hueDevicesLoading.show(); } const refreshQuery = forceRefresh ? '&forceRefresh=1' : ''; $.getJSON(`KNXUltimateGetResourcesHUE?rtype=light&serverId=${encodeURIComponent(hueServer.id)}${refreshQuery}&_=${Date.now()}`, (data) => { const listCandidates = Array.isArray(data) ? data : (Array.isArray(data?.devices) ? data.devices : []); cachedHueDevices = listCandidates.map((value) => { const deviceObject = value.deviceObject || value; const rawId = deviceObject?.id || value.id || value.rid || ''; const trimmedId = typeof rawId === 'string' ? rawId.trim() : rawId; if (trimmedId === undefined || trimmedId === null || String(trimmedId).trim() === '') return null; return { id: String(trimmedId).trim(), name: value.name || value.metadata?.name || deviceObject?.metadata?.name || '', deviceType: deviceObject?.type || value.type || '', deviceObject }; }).filter(Boolean); node._cachedHueLightDevices = cachedHueDevices; applyHueDevicesPlaceholder(cachedHueDevices.length > 0); if (typeof respond === 'function') respond(filterHueDevices(cachedHueDevices, term)); }).always(() => { if ($hueDevicesLoading.length) { $hueDevicesLoading.hide(); } }).fail(() => { cachedHueDevices = []; node._cachedHueLightDevices = cachedHueDevices; applyHueDevicesPlaceholder(false); if ($hueDevicesLoading.length) { $hueDevicesLoading.hide(); } if (typeof respond === 'function') respond([]); node.__locateSessionInfo = null; updateLocateButtonState(false); }); }; // TIMER BLINK #################################################### let blinkStatus = 2; let timerBlinkBackground; function blinkBackground(_elementIDwithHashAtTheBeginning) { if (timerBlinkBackground !== undefined) clearInterval(timerBlinkBackground); timerBlinkBackground = setInterval(() => { if (isEven(blinkStatus)) $(_elementIDwithHashAtTheBeginning).css("background-color", "lightgreen"); if (!isEven(blinkStatus)) $(_elementIDwithHashAtTheBeginning).css("background-color", ""); blinkStatus += 1; if (blinkStatus >= 14) { clearInterval(timerBlinkBackground); blinkStatus = 2; $(_elementIDwithHashAtTheBeginning).css("background-color", ""); } }, 100); } function isEven(n) { return (n % 2 == 0); } // ################################################################ const $effectStorage = $("#node-input-effectRules"); let parsedEffectRules = []; if (Array.isArray(node.effectRules)) { parsedEffectRules = JSON.parse(JSON.stringify(node.effectRules)); } else { try { parsedEffectRules = JSON.parse(node.effectRules || '[]'); } catch (error) { parsedEffectRules = []; } } if (!Array.isArray(parsedEffectRules)) parsedEffectRules = []; node.effectRules = parsedEffectRules; const $effectList = $("#node-input-effect-rule-container"); const syncEffectRulesStorage = () => { try { $effectStorage.val(JSON.stringify(node.effectRules || [])); } catch (error) { $effectStorage.val('[]'); } }; const rebuildEffectRulesFromUI = () => { const collected = []; try { const items = $effectList.editableList('items'); items.each(function () { const $row = $(this); const knxValue = $row.find('.rowEffectKNXValue').val(); const hueEffect = $row.find('.rowEffectHueEffect').val(); if (hueEffect && hueEffect !== '') { collected.push({ knxValue: knxValue !== undefined && knxValue !== null ? knxValue : '', hueEffect }); } }); } catch (error) { } node.effectRules = collected; syncEffectRulesStorage(); }; const bindEffectRowEvents = ($row) => { $row.find('.rowEffectKNXValue').on('input change', rebuildEffectRulesFromUI); $row.find('.rowEffectHueEffect').on('change', rebuildEffectRulesFromUI); }; syncEffectRulesStorage(); const $effectContainer = $("#divHueEffectsContainer"); const $effectContent = $("#divHueEffectsContent"); const $effectNoSupport = $("#divHueEffectsNoSupport"); let effectOptions = []; const normalizeEffectEntry = (effect, { fallbackLabel } = {}) => { if (effect === undefined || effect === null) return null; if (typeof effect === 'string') { const trimmed = effect.trim(); return trimmed === '' ? null : { value: trimmed, label: trimmed }; } if (typeof effect === 'object') { const nested = (candidate) => { if (!candidate || typeof candidate !== 'object') return undefined; return candidate.value ?? candidate.effect ?? candidate.id ?? candidate.code ?? candidate.type ?? candidate.name ?? candidate.title; }; const statusCandidate = typeof effect.status === 'object' ? nested(effect.status) : undefined; const rawValue = effect.value ?? effect.effect ?? statusCandidate ?? effect.status ?? effect.id ?? effect.type ?? effect.code; const rawLabel = effect.label ?? effect.name ?? effect.title ?? effect.display ?? effect.display_name ?? effect.text ?? effect.description ?? (typeof effect.status === 'object' ? (effect.status.name ?? effect.status.title ?? effect.status.label) : undefined); const value = rawValue !== undefined && rawValue !== null ? String(rawValue).trim() : ''; const labelSource = rawLabel !== undefined && rawLabel !== null ? rawLabel : (fallbackLabel !== undefined ? fallbackLabel : rawValue); const label = labelSource !== undefined && labelSource !== null ? String(labelSource).trim() : ''; if (value === '') return null; return { value, label: label === '' ? value : label }; } const stringified = String(effect).trim(); return stringified === '' ? null : { value: stringified, label: stringified }; }; const collectEffectFallbacks = () => { const fallback = []; if (Array.isArray(node.effectRules)) { node.effectRules.forEach((rule) => { const entry = normalizeEffectEntry(rule ? rule.hueEffect : null); if (entry) fallback.push(entry); }); } return fallback; }; const collectHueDeviceObjectEffects = () => { const fallback = []; if (node.hueDeviceObject && node.hueDeviceObject.effects && Array.isArray(node.hueDeviceObject.effects.status_values)) { node.hueDeviceObject.effects.status_values.forEach((raw) => { const entry = normalizeEffectEntry(raw); if (entry) fallback.push(entry); }); } return fallback; }; const getAllEffectOptions = () => { const combined = []; const pushUnique = (entry, { forceFront = false } = {}) => { if (!entry || !entry.value) return; const existingIndex = combined.findIndex((candidate) => candidate.value === entry.value); if (existingIndex !== -1) { if (forceFront && existingIndex > 0) { const [existing] = combined.splice(existingIndex, 1); combined.unshift(existing); } return; } if (forceFront) { combined.unshift(entry); } else { combined.push(entry); } }; pushUnique({ value: 'no_effect', label: 'no_effect' }, { forceFront: true }); effectOptions.forEach((option) => pushUnique(option)); collectHueDeviceObjectEffects().forEach((option) => pushUnique(option)); collectEffectFallbacks().forEach((option) => pushUnique(option)); return combined; }; function populateEffectSelect($select, selectedValue) { const targetValue = selectedValue !== undefined && selectedValue !== null ? String(selectedValue).trim() : ''; const entries = getAllEffectOptions(); $select.empty(); entries.forEach((entry) => { $select.append($("<option></option>").attr("value", entry.value).text(entry.label)); }); if (targetValue && entries.some((entry) => entry.value === targetValue)) { $select.val(targetValue); } else if (!targetValue && entries.length > 0) { $select.val(entries[0].value); } return entries.map((entry) => entry.value); } function decorateEffectValueInput() { } function refreshEffectRows() { if (!$effectList.data('effectListInitialized')) return; const items = $effectList.editableList('items'); items.each(function () { const $row = $(this); const $select = $row.find('select.rowEffectHueEffect'); const currentValue = $select.val(); populateEffectSelect($select, currentValue); decorateEffectValueInput($row.find('.rowEffectKNXValue')); }); } function setAvailableEffects(effects) { const sanitized = []; if (Array.isArray(effects)) { effects.forEach((raw) => { const entry = normalizeEffectEntry(raw); if (entry) sanitized.push(entry); }); } effectOptions = sanitized; const hasMappings = Array.isArray(node.effectRules) && node.effectRules.length > 0; const hasOptions = effectOptions.length > 0; if (!hasOptions && !hasMappings) { $effectContainer.show(); $effectContent.hide(); $effectNoSupport.show(); } else { $effectContainer.show(); $effectContent.show(); $effectNoSupport.toggle(!hasOptions); } $("#node-input-effect-autofill").prop('disabled', !hasOptions); refreshEffectRows(); } function decorateEffectValueInput() { } function ensureEffectEditableList() { if ($effectList.data('effectListInitialized')) return; $effectList.editableList({ addItem: function (container, i, opt) { const data = opt && opt.rule ? opt.rule : (opt || {}); const row = $('<div/>', { class: 'form-row effect-rule-row' }).appendTo(container); const $valueInput = $('<input/>', { class: 'rowEffectKNXValue', type: 'text', placeholder: node._('knxUltimateHueLight.effect_knx_value_placeholder') || 'Value', style: 'width:40%; margin-left:0; text-align:left;' }).appendTo(row); $valueInput.val(data.knxValue || ''); decorateEffectValueInput($valueInput); $('<span/>', { html: '&nbsp;&rarr;&nbsp;', style: 'display:inline-block; margin:0 8px;' }).appendTo(row); const $select = $('<select/>', { class: 'rowEffectHueEffect', style: 'width:45%;' }).appendTo(row); const availableOptions = populateEffectSelect($select, data.hueEffect); if ((!data || !data.hueEffect) && availableOptions.length > 0) { $select.val(availableOptions[0]); } bindEffectRowEvents(row); }, removable: true, sortable: false, removeItem: function () { rebuildEffectRulesFromUI(); } }); $effectList.data('effectListInitialized', true); } ensureEffectEditableList(); const initialEffects = (node.hueDeviceObject && node.hueDeviceObject.effects && Array.isArray(node.hueDeviceObject.effects.status_values)) ? node.hueDeviceObject.effects.status_values : []; setAvailableEffects(initialEffects); if (Array.isArray(node.effectRules) && node.effectRules.length > 0) { const items = $effectList.editableList('items'); items.each(function () { $(this).remove(); }); node.effectRules.forEach((rule) => { $effectList.editableList('addItem', { rule }); }); refreshEffectRows(); rebuildEffectRulesFromUI(); } $("#node-input-effect-autofill").off('click').on('click', function () { if (effectOptions.length === 0) return; const items = $effectList.editableList('items'); items.each(function () { $(this).remove(); }); effectOptions.forEach((effect) => { if (!effect || !effect.value) return; $effectList.editableList('addItem', { rule: { knxValue: effect.value, hueEffect: effect.value } }); }); refreshEffectRows(); rebuildEffectRulesFromUI(); }); $('#node-input-dptLightEffect').on('change', () => { const items = $effectList.editableList('items'); items.each(function () { decorateEffectValueInput($(this).find('.rowEffectKNXValue')); }); }); const $tabs = $("#tabs"); const $pinSectionRow = $("#node-input-enableNodePINS").closest('.form-row'); const $pinSelect = $("#node-input-enableNodePINS"); const $pinInfoRow = $pinSectionRow.next('.form-tips'); $tabs.addClass('hue-vertical-tabs'); $tabs.tabs(); // Tabs gestione KNX $tabs.find('ul').addClass('ui-tabs-nav'); $tabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left'); function getDPT(_dpt, _destinationWidget) { // DPT Switch command // ######################## const prefixes = Array.isArray(_dpt) ? _dpt : [_dpt]; $(_destinationWidget).empty(); if (!hasKnxServerSelected()) { return; } const serverId = resolveKnxServerValue(); $.getJSON(`knxUltimateDpts?serverId=${serverId}&_=${Date.now()}`, (data) => { data.forEach((dpt) => { if (prefixes.some((prefix) => prefix === "" || dpt.value.startsWith(prefix))) { // Adjustment for HUE Temperature if (dpt.value.startsWith("7.600")) { $(_destinationWidget).append($("<option></option>").attr("value", dpt.value).text(dpt.text + " - KNX Kelvin range 2000-6535k (Homeassistant color_temperature_mode: absolute)")); } else if (dpt.value.startsWith("9.002")) { $(_destinationWidget).append($("<option></option>").attr("value", dpt.value).text(dpt.text + " - HUE Kelvin range 2000-6535k (Homeassistant color_temperature_mode: absolute_float)")); } else if (dpt.value.startsWith("5.001")) { $(_destinationWidget).append($("<option></option>").attr("value", dpt.value).text(dpt.text + " - Homeassistant color_temperature_mode: relative")); } else { $(_destinationWidget).append($("<option></option>").attr("value", dpt.value).text(dpt.text)); } } }); // Eval const format = "node." + _destinationWidget.replace("#node-input-", ""); try { if (format !== undefined) $(_destinationWidget).val(eval(format).toString()); } catch (error) { } if (_destinationWidget === '#node-input-dptLightEffect') { $(_destinationWidget).trigger('change'); } }); } function getGroupAddress(_sourceWidgetAutocomplete, _destinationWidgetName, _destinationWidgetDPT, _additionalSearchTerm) { $(_sourceWidgetAutocomplete).autocomplete({ minLength: 0, source: function (request, response) { if (!hasKnxServerSelected()) { response([]); return; } const serverId = resolveKnxServerValue(); $.getJSON(`knxUltimatecsv?nodeID=${serverId}&_=${Date.now()}`, (data) => { response( $.map(data, function (value, key) { var sSearch = value.ga + " (" + value.devicename + ") DPT" + value.dpt; for (let index = 0; index < _additionalSearchTerm.length; index++) { const sDPT = _additionalSearchTerm[index]; if (htmlUtilsfullCSVSearch(sSearch, request.term + " " + sDPT)) { return { label: value.ga + " # " + value.devicename + " # " + value.dpt, // Label for Display value: value.ga, // Value }; } }; }) ); }); }, select: function (event, ui) { // Sets Datapoint and device name automatically var sDevName = ui.item.label.split("#")[1].trim(); try { sDevName = sDevName.substr(sDevName.indexOf(")") + 1).trim(); } catch (error) { } $(_destinationWidgetName).val(sDevName); var optVal = $(_destinationWidgetDPT + " option:contains('" + ui.item.label.split("#")[2].trim() + "')").attr("value"); const $dptSelect = $(_destinationWidgetDPT); if (optVal !== undefined && optVal !== null) { $dptSelect.val(optVal).trigger('change'); } else { // Ensure downstream widgets refresh even when the DPT is missing $dptSelect.trigger('change'); } }, }).focus(function () { $(this).autocomplete('search', $(this).val() + 'exactmatch'); }); try { if (hasKnxServerSelected()) { const srv = RED.nodes.node(resolveKnxServerValue()); if (srv && srv.id) KNX_enableSecureFormatting($(_sourceWidgetAutocomplete), srv.id); } } catch (e) { } } const effectDptPrefixes = ["1.", "2.", "5.", "6.", "7.", "8.", "9.", "16.", "20."]; const refreshKnxBindings = () => { getDPT("1.", "#node-input-dptLightSwitch"); getGroupAddress("#node-input-GALightSwitch", "#node-input-nameLightSwitch", "#node-input-dptLightSwitch", ["1."]); getDPT("1.", "#node-input-dptLightState"); getGroupAddress("#node-input-GALightState", "#node-input-nameLightState", "#node-input-dptLightState", ["1."]); getDPT("3.007", "#node-input-dptLightDIM"); getGroupAddress("#node-input-GALightDIM", "#node-input-nameLightDIM", "#node-input-dptLightDIM", ["3.007"]); getDPT("5.001", "#node-input-dptLightBrightness"); getGroupAddress("#node-input-GALightBrightness", "#node-input-nameLightBrightness", "#node-input-dptLightBrightness", ["5.001"]); getDPT("5.001", "#node-input-dptLightBrightnessState"); getGroupAddress("#node-input-GALightBrightnessState", "#node-input-nameLightBrightnessState", "#node-input-dptLightBrightnessState", ["5.001"]); getDPT("232.600", "#node-input-dptLightColor"); getGroupAddress("#node-input-GALightColor", "#node-input-nameLightColor", "#node-input-dptLightColor", ["232.600"]); getDPT("232.600", "#node-input-dptLightColorState"); getGroupAddress("#node-input-GALightColorState", "#node-input-nameLightColorState", "#node-input-dptLightColorState", ["232.600"]); getDPT("3.007", "#node-input-dptLightKelvinDIM"); getGroupAddress("#node-input-GALightKelvinDIM", "#node-input-nameLightKelvinDIM", "#node-input-dptLightKelvinDIM", ["3.007"]); getDPT("5.001", "#node-input-dptLightKelvinPercentage"); getGroupAddress("#node-input-GALightKelvinPercentage", "#node-input-nameLightKelvinPercentage", "#node-input-dptLightKelvinPercentage", ["5.001"]); getDPT("5.001", "#node-input-dptLightKelvinPercentageState"); getGroupAddress("#node-input-GALightKelvinPercentageState", "#node-input-nameLightKelvinPercentageState", "#node-input-dptLightKelvinPercentageState", ["5.001"]); getDPT("1.", "#node-input-dptLightBlink"); getGroupAddress("#node-input-GALightBlink", "#node-input-nameLightBlink", "#node-input-dptLightBlink", ["1."]); getDPT("1.", "#node-input-dptLightColorCycle"); getGroupAddress("#node-input-GALightColorCycle", "#node-input-nameLightColorCycle", "#node-input-dptLightColorCycle", ["1."]); getDPT(effectDptPrefixes, "#node-input-dptLightEffect"); getGroupAddress("#node-input-GALightEffect", "#node-input-nameLightEffect", "#node-input-dptLightEffect", effectDptPrefixes); getDPT(effectDptPrefixes, "#node-input-dptLightEffectStatus"); getGroupAddress("#node-input-GALightEffectStatus", "#node-input-nameLightEffectStatus", "#node-input-dptLightEffectStatus", effectDptPrefixes); getDPT("1.", "#node-input-dptDaylightSensor"); getGroupAddress("#node-input-GADaylightSensor", "#node-input-nameDaylightSensor", "#node-input-dptDaylightSensor", ["1."]); getDPT("7.600", "#node-input-dptLightKelvin"); getDPT("9.002", "#node-input-dptLightKelvin"); getDPT("9.002", "#node-input-dptLightKelvinState"); getDPT("7.600", "#node-input-dptLightKelvinState"); getGroupAddress("#node-input-GALightKelvin", "#node-input-nameLightKelvin", "#node-input-dptLightKelvin", ["7.600", "9.002"]); getGroupAddress("#node-input-GALightKelvinState", "#node-input-nameLightKelvinState", "#node-input-dptLightKelvinState", ["7.600", "9.002"]); // HSV ---------------------- // H getDPT("3.007", "#node-input-dptLightHSV_H_DIM"); getGroupAddress("#node-input-GALightHSV_H_DIM", "#node-input-nameLightHSV_H_DIM", "#node-input-dptLightHSV_H_DIM", ["3.007"]); getDPT("5.001", "#node-input-dptLightHSV_H_State"); getGroupAddress("#node-input-GALightHSV_H_State", "#node-input-nameLightHSV_H_State", "#node-input-dptLightHSV_H_State", ["5.001"]); // S getDPT("3.007", "#node-input-dptLightHSV_S_DIM"); getGroupAddress("#node-input-GALightHSV_S_DIM", "#node-input-nameLightHSV_S_DIM", "#node-input-dptLightHSV_S_DIM", ["3.007"]); getDPT("5.001", "#node-input-dptLightHSV_S_State"); getGroupAddress("#node-input-GALightHSV_S_State", "#node-input-nameLightHSV_S_State", "#node-input-dptLightHSV_S_State", ["5.001"]); // V getDPT("3.007", "#node-input-dptLightHSV_V_DIM"); getGroupAddress("#node-input-GALightHSV_V_DIM", "#node-input-nameLightHSV_V_DIM", "#node-input-dptLightHSV_V_DIM", ["3.007"]); getDPT("5.001", "#node-input-dptLightHSV_V_State"); getGroupAddress("#node-input-GALightHSV_V_State", "#node-input-nameLightHSV_V_State", "#node-input-dptLightHSV_V_State", ["5.001"]); // END HSV ---------------------- }; refreshKnxBindings(); const getHueDeviceValue = ({ allowStored = false } = {}) => { if ($hueDeviceInput.length) { const domValue = $hueDeviceInput.val(); if (domValue !== undefined && domValue !== null && domValue !== '') { return domValue; } } if (node.hueDevice !== undefined && node.hueDevice !== null && node.hueDevice !== '') { return node.hueDevice; } if (allowStored && node.__locateSessionInfo && node.__locateSessionInfo.deviceId) { const suffix = node.__locateSessionInfo.deviceType || 'light'; return `${node.__locateSessionInfo.deviceId}#${suffix}`; } return ''; }; const updateTabsVisibility = () => { const knxSelected = hasKnxServerSelected(); const hueDeviceSelected = getHueDeviceValue() !== ''; const shouldShowTabs = knxSelected && hueDeviceSelected; if (shouldShowTabs) { $tabs.show(); try { $tabs.tabs('refresh'); } catch (error) { /* empty */ } } else { $tabs.hide(); } if ($pinSelect.length) { const desiredPins = knxSelected ? 'no' : 'yes'; if ($pinSelect.val() !== desiredPins) { $pinSelect.val(desiredPins).trigger('change'); } } if ($pinSectionRow.length) { $pinSectionRow.show(); } if ($pinInfoRow.length) { $pinInfoRow.show(); } }; updateTabsVisibility(); $knxServerInput.on('change.knxUltimateHueLight', () => { refreshKnxBindings(); updateTabsVisibility(); }); $hueDeviceInput.on('change.knxUltimateHueLight input.knxUltimateHueLight', updateTabsVisibility); if (($deviceNameInput.val() || '').trim() !== '') { applyHueDevicesPlaceholder(true); } else { applyHueDevicesPlaceholder(cachedHueDevices.length > 0); } if ($deviceNameInput.length) { if ($deviceNameInput.data('ui-autocomplete')) { try { $deviceNameInput.autocomplete('destroy'); } catch (error) { /* empty */ } } $deviceNameInput.autocomplete({ minLength: 0, source(request, response) { fetchHueDevices(request.term, response); }, select(event, ui) { if (!ui.item || !ui.item.hueDevice || ui.item.hueDevice === 'error') { event.preventDefault(); return; } $deviceNameInput.val(ui.item.value || ''); node.name = ui.item.value || node.name; $hueDeviceInput.val(ui.item.hueDevice); node.hueDevice = ui.item.hueDevice; node.hueDeviceObject = ui.item.deviceObject || { type: ui.item.deviceType }; node.__locateSessionInfo = null; updateTabsVisibility(); setTimeout(() => { try { $deviceNameInput.autocomplete('close'); } catch (error) { /* empty */ } Go(); }, 0); }, focus(event, ui) { event.preventDefault(); $deviceNameInput.val(ui.item && ui.item.value ? ui.item.value : ''); } }).focus(function () { $(this).autocomplete('search', `${$(this).val()}exactmatch`); }).on('input.knxUltimateHueLight', function () { if ($(this).val().trim() === '') { $hueDeviceInput.val(''); node.hueDevice = ''; node.__locateSessionInfo = null; updateTabsVisibility(); updateLocateButtonState(false); } }); } if ($refreshDevicesButton.length) { $refreshDevicesButton.off('.knxUltimateHueLight').on('click.knxUltimateHueLight', () => { cachedHueDevices = []; node._cachedHueLightDevices = cachedHueDevices; fetchHueDevices('', () => { if ($deviceNameInput.length) { $deviceNameInput.autocomplete('search', `${$deviceNameInput.val()}exactmatch`); } }, { forceRefresh: true }); });