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,102 lines (993 loc) • 107 kB
HTML
'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 $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']);
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 = () => {
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;
}
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([]);
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();
}
$.getJSON(`KNXUltimateGetResourcesHUE?rtype=light&serverId=${encodeURIComponent(hueServer.id)}&_=${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([]);
});
};
// 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: ' → ', 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 = () => {
const domValue = $hueDeviceInput.val();
if (domValue !== undefined && domValue !== null && domValue !== '') {
return domValue;
}
if (node.hueDevice !== undefined && node.hueDevice !== null) {
return node.hueDevice;
}
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 (!knxSelected && $pinSelect.length && $pinSelect.val() !== 'yes') {
$pinSelect.val('yes').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 };
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 = '';
updateTabsVisibility();
}
});
}
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 ($hueServerInput.length) {
$hueServerInput.off('.knxUltimateHueLightDevices').on('change.knxUltimateHueLightDevices', () => {
cachedHueDevices = [];
node._cachedHueLightDevices = cachedHueDevices;
showingNoHueDevicesPlaceholder = false;
applyHueDevicesPlaceholder(true);
if ($hueDevicesLoading.length) {
$hueDevicesLoading.hide();
}
});
}
// Get the HUE capabilities to enable/disable UI parts
var getJsonPromise;
const initialHueDeviceRaw = getHueDeviceValue();
if (initialHueDeviceRaw === '') {
updateTabsVisibility();
return;
} else {
$hueDeviceInput.val(initialHueDeviceRaw);
updateTabsVisibility();
if (getJsonPromise !== undefined) getJsonPromise.abort();
getJsonPromise = $.getJSON("knxUltimateGetLightObject?id=" + initialHueDeviceRaw.split("#")[0] + "&serverId=" + $("#node-input-serverHue").val() + "&" + { _: new Date().getTime() }, (data) => {
let oLight = data;
const effects = (oLight && oLight.effects && Array.isArray(oLight.effects.status_values))
? oLight.effects.status_values
: [];
setAvailableEffects(effects);
// Check if grouped, to hide/show the "Get current" buttons
if (oLight.type === "grouped_light") {
$("#tabs").tabs("enable", "#tabs-4");
$("#tabs").tabs("enable", "#tabs-3");
$("#tabs").tabs("enable", "#tabs-2");
$("#getColorAtSwitchOnDayTimeButton").show();
$("#getColorAtSwitchOnNightTimeButton").show();
$("#node-input-specifySwitchOnBrightness").empty().append(
$("<option>")
.val("no")
.text(node._("knxUltimateHueLight.none"))
).append(
$("<option>")
.val("yes")
.text(node._("knxUltimateHueLight.select_color"))
).append(
$("<option>")
.val("temperature")
.text(node._("knxUltimateHueLight.select_temperature_brightness"))
);
$("#node-input-enableDayNightLighting").empty().append(
$("<option>")
.val("no")
.text(node._("knxUltimateHueLight.opt_no"))
).append(
$("<option>")
.val("yes")
.text(node._("knxUltimateHueLight.select_color"))
).append(
$("<option>")
.val("temperature")
.text(node._("knxUltimateHueLight.select_temperature_brightness"))
);
$("#node-input-specifySwitchOnBrightness").val(node.specifySwitchOnBrightness).trigger('change');
$("#node-input-enableDayNightLighting").val(node.enableDayNightLighting).trigger('change');
return;
} else {
$("#getColorAtSwitchOnDayTimeButton").show();
$("#getColorAtSwitchOnNightTimeButton").show();
$("#node-input-specifySwitchOnBrightness").empty().append(
$("<option>")
.val("no")
.text(node._("knxUltimateHueLight.none"))
);
$("#node-input-enableDayNightLighting").empty().append(
$("<option>")
.val("no")
.text(node._("knxUltimateHueLight.opt_no"))
);
}
$("#tabs").tabs("disable", "#tabs-4");
$("#tabs").tabs("disable", "#tabs-3");
$("#tabs").tabs("disable", "#tabs-2");
$("#divColorsAtSwitchOn").hide();
$("#divColorsAtSwitchOnNightTime").hide();
$("#divTemperatureAtSwitchOn").hide();
$("#divTemperatureAtSwitchOnNightTime").hide();
$("#divColorCycle").hide();
$("#divUpdateKNXBrightnessStatusOnHUEOnOff").hide();
$("#divBehaviourBrightness").hide();
$("#comboTemperatureAtSwitchOn").hide();
$("#comboTemperatureAtSwitchOnNightTime").hide();
// Enable options/tabs one by one
if (oLight.dimming !== undefined) {
$("#tabs").tabs("enable", "#tabs-2");
$("#divBehaviourBrightness").show();
}
if (oLight.color !== undefined) {
$("#tabs").tabs("enable", "#tabs-4");
$("#divColorsAtSwitchOn").show();
$("#divColorsAtSwitchOnNightTime").show();
$("#divColorCycle").show();
$("#node-input-specifySwitchOnBrightness").append(
$("<option>")
.val("yes")
.text(node._("knxUltimateHueLight.select_color"))
);
$("#node-input-enableDayNightLighting").append(
$("<option>")
.val("yes")
.text(node._("knxUltimateHueLight.select_color"))
);
}
// Check temperature (if the light supports temperature, it support dimming as well)
if (oLight.color_temperature !== undefined) {
$("#tabs").tabs("enable", "#tabs-3");
//$("#tabs").tabs("enable", "#tabs-2");
$("#node-input-specifySwitchOnBrightness").append(
$("<option>")
.val("temperature")
.text(node._("knxUltimateHueLight.select_temperature_brightness"))
);
$("#node-input-enableDayNightLighting").append(
$("<option>")
.val("temperature")
.text(node._("knxUltimateHueLight.select_temperature_brightness"))
);
$("#divTemperatureAtSwitchOn").show();
$("#divTemperatureAtSwitchOnNightTime").show();
$("#divUpdateKNXBrightnessStatusOnHUEOnOff").show();
$("#divBehaviourBrightness").show();
$("#comboTemperatureAtSwitchOn").show();
$("#comboTemperatureAtSwitchOnNightTime").show();
} else {
//$("#tabs").tabs("enable", "#tabs-2");
$("#node-input-specifySwitchOnBrightness").append(
$("<option>")
.val("temperature")
.text(node._("knxUltimateHueLight.select_brightness"))
);
$("#node-input-enableDayNightLighting").append(
$("<option>")
.val("temperature")
.text(node._("knxUltimateHueLight.select_brightness"))
);
$("#comboTemperatureAtSwitchOn").val(0);
$("#comboTemperatureAtSwitchOnNightTime").val(0);
$("#divTemperatureAtSwitchOn").show();
$("#divTemperatureAtSwitchOnNightTime").show();
$("#divUpdateKNXBrightnessStatusOnHUEOnOff").show();
//$("#divBehaviourBrightness").show();
}
$("#node-input-specifySwitchOnBrightness").val(node.specifySwitchOnBrightness).trigger('change');
$("#node-input-enableDayNightLighting").val(node.enableDayNightLighting).trigger('change');
});
setTimeout(function () { if (getJsonPromise !== undefined) getJsonPromise.abort(); }, 10000);
}
// Show/Hide the div of the color at swich on
if (node.specifySwitchOnBrightness === "yes") {
$("#divColorsAtSwitchOn").show();
$("#divTemperatureAtSwitchOn").hide();
} else if (node.specifySwitchOnBrightness === "temperature") {
$("#divColorsAtSwitchOn").hide();
$("#divTemperatureAtSwitchOn").show();
} else {
$("#divColorsAtSwitchOn").hide();
$("#divTemperatureAtSwitchOn").hide();
}
$("#node-input-specifySwitchOnBrightness").on("change", function () {
if ($("#node-input-specifySwitchOnBrightness").val() === "yes") {
$("#divColorsAtSwitchOn").show();
$("#divTemperatureAtSwitchOn").hide();
blinkBackground("#colorPickerDay");
} else if ($("#node-input-specifySwitchOnBrightness").val() === "temperature") {
$("#divColorsAtSwitchOn").hide();
$("#divTemperatureAtSwitchOn").show();
} else {
$("#divColorsAtSwitchOn").hide();
$("#divTemperatureAtSwitchOn").hide();
}
});
// Show/Hide and enable/disable day/night Lighting behaviour
if (node.enableDayNightLighting === "yes") {
$("#divEnableDayNightLighting").show();
$("#divCCSBoxAtNightLighting").css({ border: "1px solid dimgrey", "border-radius": "12px", padding: "5px" }); // Add little box to better understand the property page
$("#divColo