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 and ETS group address importer. Easy to use and highly configurable.

1,115 lines (1,004 loc) 119 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", 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" } }, 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; 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 }); }); } if ($locateDeviceButton.length) { $locateDeviceButton.off('.knxUltimateHueLight').on('click.knxUltimateHueLight', () => { const desiredAction = locateSessionActive ? 'stop' : 'start'; performLocateRequest({ silent: false, action: desiredAction }); });