Skip to content

Commit c57818d

Browse files
committed
.
1 parent efd6142 commit c57818d

15 files changed

+1195
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
**Version 4.0.11** - October 2025<br/>
1010
- HUE Light node: added configuration UI and runtime support for Hue lamp dynamic effects (candle, fireplace, etc.) with KNX mappings and status feedback.<br/>
11+
- NEW: Added Hue Plug/Outlet node to map KNX on/off control to Philips Hue smart plugs.<br/>
1112

1213
**Version 4.0.10** - October 2025<br/>
1314
- KNX Config node: you can now choose wether to display only the errors in the node statuses only errors.<br/>
14-
- HUE Light node: added configuration UI and runtime support for Hue lamp dynamic effects (candle, fireplace, etc.) with KNX mappings and status feedback.<br/>
1515

1616
**Version 4.0.9** - September 2025<br/>
1717
- KNX Config node: now the ethernet interface is automatically selected, based on the KNX Gateway's IP subnet.<br/>

nodes/hue-config.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ module.exports = (RED) => {
246246
let allResources;
247247
if (_rtype === "light" || _rtype === "grouped_light") {
248248
allResources = node.hueAllResources.filter((a) => a.type === "light" || a.type === "grouped_light");
249+
} else if (_rtype === "plug") {
250+
allResources = node.hueAllResources.filter((a) => a.type === "plug" || a.type === "smartplug" || a.type === "smart_plug");
251+
console.log('getResources plug raw resources', allResources.map((res) => ({ id: res.id, type: res.type, owner: res.owner?.rtype })));
249252
} else {
250253
allResources = node.hueAllResources.filter((a) => a.type === _rtype);
251254
}
@@ -324,6 +327,19 @@ module.exports = (RED) => {
324327
id: resource.id,
325328
});
326329
}
330+
if (_rtype === "plug") {
331+
const linkedDevice = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id));
332+
const room = node.hueAllRooms?.find((roomItem) => roomItem.children?.find((child) => child.rid === linkedDevice?.id));
333+
const plugName = linkedDevice?.metadata?.name || resource.metadata?.name || "Unnamed Plug";
334+
const stateLabel = resource?.on?.on === true ? "on" : "off";
335+
console.log('getResources plug direct resource', { id: resource.id, type: resource.type, name: plugName, linkedDevice: linkedDevice?.id });
336+
retArray.push({
337+
name: `Plug: ${plugName}${room !== undefined ? `, room ${room.metadata.name}` : ""} [${stateLabel}]`,
338+
id: resource.id,
339+
type: resource.type,
340+
deviceObject: resource,
341+
});
342+
}
327343
if (_rtype === "temperature") {
328344
const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid));
329345
const linkedDevName = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || "";
@@ -379,6 +395,38 @@ module.exports = (RED) => {
379395
});
380396
}
381397
}
398+
if (_rtype === "plug" && retArray.length === 0) {
399+
const plugDevices = node.hueAllResources.filter((dev) => {
400+
if (dev.type !== "device" || !Array.isArray(dev.services)) return false;
401+
const archetypePlug = dev.product_data?.product_archetype === 'plug' || dev.metadata?.archetype === 'plug' || /plug/i.test(dev.product_data?.product_name || '') || /plug/i.test(dev.metadata?.name || '');
402+
const hasService = dev.services.some((serv) => ['plug', 'smartplug', 'smart_plug', 'light'].includes(serv.rtype || ''));
403+
return archetypePlug && hasService;
404+
});
405+
plugDevices.forEach((device) => {
406+
try {
407+
const plugService = device.services.find((serv) => ['plug', 'smartplug', 'smart_plug', 'light'].includes(serv.rtype || ''));
408+
if (!plugService) return;
409+
const plugResource = node.hueAllResources.find((res) => res.id === plugService.rid) || {};
410+
const room = node.hueAllRooms?.find((roomItem) => roomItem.children?.find((child) => child.rid === device.id));
411+
const plugName = device.metadata?.name || plugResource.metadata?.name || "Unnamed Plug";
412+
const stateLabel = plugResource?.on?.on === true ? "on" : (plugResource?.on?.on === false ? "off" : "");
413+
retArray.push({
414+
name: `Plug: ${plugName}${room !== undefined ? `, room ${room.metadata.name}` : ""}${stateLabel ? ` [${stateLabel}]` : ""}`,
415+
id: plugService.rid || device.id,
416+
type: plugService.rtype || plugResource.type || 'light',
417+
deviceObject: plugResource.on ? plugResource : {
418+
id: plugService.rid || device.id,
419+
type: plugService.rtype || plugResource.type || 'light',
420+
on: plugResource.on,
421+
owner: { rid: device.id, rtype: 'device' },
422+
},
423+
});
424+
} catch (err) {
425+
node.sysLogger?.warn(`KNXUltimateHue: getResources plug fallback error ${err.message}`);
426+
}
427+
});
428+
}
429+
node.sysLogger?.debug(`getResources plug returning ${retArray.length}`);
382430
return { devices: retArray };
383431
} catch (error) {
384432
node.sysLogger?.error(`KNXUltimateHue: hueEngine: classHUE: getResources: error ${error.message}`);

nodes/knxUltimateHueLight.html

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
// Ignore UI quirks for legacy Node-RED versions
155155
}
156156
};
157-
["#node-input-server", "#node-input-serverHue"].forEach(ensureConfigSelection);
157+
["#node-input-serverHue"].forEach(ensureConfigSelection);
158158

159159
function ensureVerticalTabsStyle() {
160160
if ($('#knxUltimateHueLightVerticalTabs').length) return;
@@ -203,6 +203,25 @@
203203

204204
function onEditPrepare() {
205205
ensureVerticalTabsStyle();
206+
const $knxServerInput = $("#node-input-server");
207+
const KNX_EMPTY_VALUES = new Set(['', 'none', '_ADD_', '__NONE__']);
208+
209+
const resolveKnxServerValue = () => {
210+
const domValue = $knxServerInput.val();
211+
if (domValue !== undefined && domValue !== null && domValue !== '') {
212+
return domValue;
213+
}
214+
if (node.server !== undefined && node.server !== null) {
215+
return node.server;
216+
}
217+
return '';
218+
};
219+
220+
const hasKnxServerSelected = () => {
221+
const val = resolveKnxServerValue();
222+
if (val === undefined || val === null) return false;
223+
return !KNX_EMPTY_VALUES.has(val);
224+
};
206225
// TIMER BLINK ####################################################
207226
let blinkStatus = 2;
208227
let timerBlinkBackground;
@@ -408,7 +427,11 @@
408427
// ########################
409428
const prefixes = Array.isArray(_dpt) ? _dpt : [_dpt];
410429
$(_destinationWidget).empty();
411-
$.getJSON("knxUltimateDpts?serverId=" + $("#node-input-server").val() + "&" + { _: new Date().getTime() }, (data) => {
430+
if (!hasKnxServerSelected()) {
431+
return;
432+
}
433+
const serverId = resolveKnxServerValue();
434+
$.getJSON(`knxUltimateDpts?serverId=${serverId}&_=${Date.now()}`, (data) => {
412435
data.forEach((dpt) => {
413436
if (prefixes.some((prefix) => prefix === "" || dpt.value.startsWith(prefix))) {
414437
// Adjustment for HUE Temperature
@@ -438,8 +461,12 @@
438461
$(_sourceWidgetAutocomplete).autocomplete({
439462
minLength: 0,
440463
source: function (request, response) {
441-
//$.getJSON("csv", request, function( data, status, xhr ) {
442-
$.getJSON("knxUltimatecsv?nodeID=" + $("#node-input-server").val() + "&" + { _: new Date().getTime() }, (data) => {
464+
if (!hasKnxServerSelected()) {
465+
response([]);
466+
return;
467+
}
468+
const serverId = resolveKnxServerValue();
469+
$.getJSON(`knxUltimatecsv?nodeID=${serverId}&_=${Date.now()}`, (data) => {
443470
response(
444471
$.map(data, function (value, key) {
445472
var sSearch = value.ga + " (" + value.devicename + ") DPT" + value.dpt;
@@ -475,84 +502,97 @@
475502
}).focus(function () {
476503
$(this).autocomplete('search', $(this).val() + 'exactmatch');
477504
});
478-
try { var srv = RED.nodes.node($("#node-input-server").val()); if (srv && srv.id) KNX_enableSecureFormatting($(_sourceWidgetAutocomplete), srv.id); } catch (e) {}
505+
try {
506+
if (hasKnxServerSelected()) {
507+
const srv = RED.nodes.node(resolveKnxServerValue());
508+
if (srv && srv.id) KNX_enableSecureFormatting($(_sourceWidgetAutocomplete), srv.id);
509+
}
510+
} catch (e) {}
479511
}
480512

481-
getDPT("1.", "#node-input-dptLightSwitch");
482-
getGroupAddress("#node-input-GALightSwitch", "#node-input-nameLightSwitch", "#node-input-dptLightSwitch", ["1."]);
513+
const effectDptPrefixes = ["1.", "2.", "5.", "6.", "7.", "8.", "9.", "16.", "20."];
483514

484-
getDPT("1.", "#node-input-dptLightState");
485-
getGroupAddress("#node-input-GALightState", "#node-input-nameLightState", "#node-input-dptLightState", ["1."]);
515+
const refreshKnxBindings = () => {
516+
getDPT("1.", "#node-input-dptLightSwitch");
517+
getGroupAddress("#node-input-GALightSwitch", "#node-input-nameLightSwitch", "#node-input-dptLightSwitch", ["1."]);
486518

487-
getDPT("3.007", "#node-input-dptLightDIM");
488-
getGroupAddress("#node-input-GALightDIM", "#node-input-nameLightDIM", "#node-input-dptLightDIM", ["3.007"]);
519+
getDPT("1.", "#node-input-dptLightState");
520+
getGroupAddress("#node-input-GALightState", "#node-input-nameLightState", "#node-input-dptLightState", ["1."]);
489521

490-
getDPT("5.001", "#node-input-dptLightBrightness");
491-
getGroupAddress("#node-input-GALightBrightness", "#node-input-nameLightBrightness", "#node-input-dptLightBrightness", ["5.001"]);
522+
getDPT("3.007", "#node-input-dptLightDIM");
523+
getGroupAddress("#node-input-GALightDIM", "#node-input-nameLightDIM", "#node-input-dptLightDIM", ["3.007"]);
492524

493-
getDPT("5.001", "#node-input-dptLightBrightnessState");
494-
getGroupAddress("#node-input-GALightBrightnessState", "#node-input-nameLightBrightnessState", "#node-input-dptLightBrightnessState", ["5.001"]);
525+
getDPT("5.001", "#node-input-dptLightBrightness");
526+
getGroupAddress("#node-input-GALightBrightness", "#node-input-nameLightBrightness", "#node-input-dptLightBrightness", ["5.001"]);
495527

496-
getDPT("232.600", "#node-input-dptLightColor");
497-
getGroupAddress("#node-input-GALightColor", "#node-input-nameLightColor", "#node-input-dptLightColor", ["232.600"]);
528+
getDPT("5.001", "#node-input-dptLightBrightnessState");
529+
getGroupAddress("#node-input-GALightBrightnessState", "#node-input-nameLightBrightnessState", "#node-input-dptLightBrightnessState", ["5.001"]);
498530

499-
getDPT("232.600", "#node-input-dptLightColorState");
500-
getGroupAddress("#node-input-GALightColorState", "#node-input-nameLightColorState", "#node-input-dptLightColorState", ["232.600"]);
531+
getDPT("232.600", "#node-input-dptLightColor");
532+
getGroupAddress("#node-input-GALightColor", "#node-input-nameLightColor", "#node-input-dptLightColor", ["232.600"]);
501533

502-
getDPT("3.007", "#node-input-dptLightKelvinDIM");
503-
getGroupAddress("#node-input-GALightKelvinDIM", "#node-input-nameLightKelvinDIM", "#node-input-dptLightKelvinDIM", ["3.007"]);
534+
getDPT("232.600", "#node-input-dptLightColorState");
535+
getGroupAddress("#node-input-GALightColorState", "#node-input-nameLightColorState", "#node-input-dptLightColorState", ["232.600"]);
504536

505-
getDPT("5.001", "#node-input-dptLightKelvinPercentage");
506-
getGroupAddress("#node-input-GALightKelvinPercentage", "#node-input-nameLightKelvinPercentage", "#node-input-dptLightKelvinPercentage", ["5.001"]);
537+
getDPT("3.007", "#node-input-dptLightKelvinDIM");
538+
getGroupAddress("#node-input-GALightKelvinDIM", "#node-input-nameLightKelvinDIM", "#node-input-dptLightKelvinDIM", ["3.007"]);
507539

508-
getDPT("5.001", "#node-input-dptLightKelvinPercentageState");
509-
getGroupAddress("#node-input-GALightKelvinPercentageState", "#node-input-nameLightKelvinPercentageState", "#node-input-dptLightKelvinPercentageState", ["5.001"]);
540+
getDPT("5.001", "#node-input-dptLightKelvinPercentage");
541+
getGroupAddress("#node-input-GALightKelvinPercentage", "#node-input-nameLightKelvinPercentage", "#node-input-dptLightKelvinPercentage", ["5.001"]);
510542

511-
getDPT("1.", "#node-input-dptLightBlink");
512-
getGroupAddress("#node-input-GALightBlink", "#node-input-nameLightBlink", "#node-input-dptLightBlink", ["1."]);
543+
getDPT("5.001", "#node-input-dptLightKelvinPercentageState");
544+
getGroupAddress("#node-input-GALightKelvinPercentageState", "#node-input-nameLightKelvinPercentageState", "#node-input-dptLightKelvinPercentageState", ["5.001"]);
513545

514-
getDPT("1.", "#node-input-dptLightColorCycle");
515-
getGroupAddress("#node-input-GALightColorCycle", "#node-input-nameLightColorCycle", "#node-input-dptLightColorCycle", ["1."]);
546+
getDPT("1.", "#node-input-dptLightBlink");
547+
getGroupAddress("#node-input-GALightBlink", "#node-input-nameLightBlink", "#node-input-dptLightBlink", ["1."]);
516548

517-
const effectDptPrefixes = ["1.", "2.", "5.", "6.", "7.", "8.", "9.", "16.", "20."];
518-
getDPT(effectDptPrefixes, "#node-input-dptLightEffect");
519-
getGroupAddress("#node-input-GALightEffect", "#node-input-nameLightEffect", "#node-input-dptLightEffect", effectDptPrefixes);
549+
getDPT("1.", "#node-input-dptLightColorCycle");
550+
getGroupAddress("#node-input-GALightColorCycle", "#node-input-nameLightColorCycle", "#node-input-dptLightColorCycle", ["1."]);
520551

521-
getDPT(effectDptPrefixes, "#node-input-dptLightEffectStatus");
522-
getGroupAddress("#node-input-GALightEffectStatus", "#node-input-nameLightEffectStatus", "#node-input-dptLightEffectStatus", effectDptPrefixes);
552+
getDPT(effectDptPrefixes, "#node-input-dptLightEffect");
553+
getGroupAddress("#node-input-GALightEffect", "#node-input-nameLightEffect", "#node-input-dptLightEffect", effectDptPrefixes);
523554

524-
getDPT("1.", "#node-input-dptDaylightSensor");
525-
getGroupAddress("#node-input-GADaylightSensor", "#node-input-nameDaylightSensor", "#node-input-dptDaylightSensor", ["1."]);
555+
getDPT(effectDptPrefixes, "#node-input-dptLightEffectStatus");
556+
getGroupAddress("#node-input-GALightEffectStatus", "#node-input-nameLightEffectStatus", "#node-input-dptLightEffectStatus", effectDptPrefixes);
526557

527-
getDPT("7.600", "#node-input-dptLightKelvin");
528-
getDPT("9.002", "#node-input-dptLightKelvin");
529-
getDPT("9.002", "#node-input-dptLightKelvinState");
530-
getDPT("7.600", "#node-input-dptLightKelvinState");
531-
getGroupAddress("#node-input-GALightKelvin", "#node-input-nameLightKelvin", "#node-input-dptLightKelvin", ["7.600", "9.002"]);
532-
getGroupAddress("#node-input-GALightKelvinState", "#node-input-nameLightKelvinState", "#node-input-dptLightKelvinState", ["7.600", "9.002"]);
558+
getDPT("1.", "#node-input-dptDaylightSensor");
559+
getGroupAddress("#node-input-GADaylightSensor", "#node-input-nameDaylightSensor", "#node-input-dptDaylightSensor", ["1."]);
533560

534-
// HSV ----------------------
535-
// H
536-
getDPT("3.007", "#node-input-dptLightHSV_H_DIM");
537-
getGroupAddress("#node-input-GALightHSV_H_DIM", "#node-input-nameLightHSV_H_DIM", "#node-input-dptLightHSV_H_DIM", ["3.007"]);
561+
getDPT("7.600", "#node-input-dptLightKelvin");
562+
getDPT("9.002", "#node-input-dptLightKelvin");
563+
getDPT("9.002", "#node-input-dptLightKelvinState");
564+
getDPT("7.600", "#node-input-dptLightKelvinState");
565+
getGroupAddress("#node-input-GALightKelvin", "#node-input-nameLightKelvin", "#node-input-dptLightKelvin", ["7.600", "9.002"]);
566+
getGroupAddress("#node-input-GALightKelvinState", "#node-input-nameLightKelvinState", "#node-input-dptLightKelvinState", ["7.600", "9.002"]);
538567

539-
getDPT("5.001", "#node-input-dptLightHSV_H_State");
540-
getGroupAddress("#node-input-GALightHSV_H_State", "#node-input-nameLightHSV_H_State", "#node-input-dptLightHSV_H_State", ["5.001"]);
568+
// HSV ----------------------
569+
// H
570+
getDPT("3.007", "#node-input-dptLightHSV_H_DIM");
571+
getGroupAddress("#node-input-GALightHSV_H_DIM", "#node-input-nameLightHSV_H_DIM", "#node-input-dptLightHSV_H_DIM", ["3.007"]);
541572

542-
// S
543-
getDPT("3.007", "#node-input-dptLightHSV_S_DIM");
544-
getGroupAddress("#node-input-GALightHSV_S_DIM", "#node-input-nameLightHSV_S_DIM", "#node-input-dptLightHSV_S_DIM", ["3.007"]);
573+
getDPT("5.001", "#node-input-dptLightHSV_H_State");
574+
getGroupAddress("#node-input-GALightHSV_H_State", "#node-input-nameLightHSV_H_State", "#node-input-dptLightHSV_H_State", ["5.001"]);
545575

546-
getDPT("5.001", "#node-input-dptLightHSV_S_State");
547-
getGroupAddress("#node-input-GALightHSV_S_State", "#node-input-nameLightHSV_S_State", "#node-input-dptLightHSV_S_State", ["5.001"]);
576+
// S
577+
getDPT("3.007", "#node-input-dptLightHSV_S_DIM");
578+
getGroupAddress("#node-input-GALightHSV_S_DIM", "#node-input-nameLightHSV_S_DIM", "#node-input-dptLightHSV_S_DIM", ["3.007"]);
548579

549-
// V
550-
getDPT("3.007", "#node-input-dptLightHSV_V_DIM");
551-
getGroupAddress("#node-input-GALightHSV_V_DIM", "#node-input-nameLightHSV_V_DIM", "#node-input-dptLightHSV_V_DIM", ["3.007"]);
580+
getDPT("5.001", "#node-input-dptLightHSV_S_State");
581+
getGroupAddress("#node-input-GALightHSV_S_State", "#node-input-nameLightHSV_S_State", "#node-input-dptLightHSV_S_State", ["5.001"]);
552582

553-
getDPT("5.001", "#node-input-dptLightHSV_V_State");
554-
getGroupAddress("#node-input-GALightHSV_V_State", "#node-input-nameLightHSV_V_State", "#node-input-dptLightHSV_V_State", ["5.001"]);
555-
// END HSV ----------------------
583+
// V
584+
getDPT("3.007", "#node-input-dptLightHSV_V_DIM");
585+
getGroupAddress("#node-input-GALightHSV_V_DIM", "#node-input-nameLightHSV_V_DIM", "#node-input-dptLightHSV_V_DIM", ["3.007"]);
586+
587+
getDPT("5.001", "#node-input-dptLightHSV_V_State");
588+
getGroupAddress("#node-input-GALightHSV_V_State", "#node-input-nameLightHSV_V_State", "#node-input-dptLightHSV_V_State", ["5.001"]);
589+
// END HSV ----------------------
590+
};
591+
592+
refreshKnxBindings();
593+
$knxServerInput.on('change.knxUltimateHueLight', () => {
594+
refreshKnxBindings();
595+
});
556596

557597
// Get the HUE capabilities to enable/disable UI parts
558598
var getJsonPromise;

0 commit comments

Comments
 (0)