203 changes: 137 additions & 66 deletions huemagic/hue-group.html
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
<script type="text/x-red" data-template-name="hue-group">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="hue-group.config.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-group.config.input-name">
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-group.config.input-name" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-bridge"><i class="fa fa-server"></i> Bridge</label>
<input type="text" id="node-input-bridge">
<input type="text" id="node-input-bridge" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-groupid"><i class="fa fa-lightbulb-o"></i> <span data-i18n="hue-group.config.group"></span></label>
<div style="display: inline-block; position: relative; width: 70%; height: 20px;">
<div style="position: absolute; left: 0px; right: 40px;">
<input type="text" id="node-input-groupid" placeholder="1" style="width: 100%"/>
<div style="display: inline-flex; width: calc(100% - 105px)">
<div id="input-select-toggle" style="flex-grow: 1;">
<input type="text" id="node-input-groupid" placeholder="00000000-0000-0000-0000-000000000000" style="width: 100%"/>
</div>
<a id="node-config-input-scan-groups" class="editor-button" style="position: absolute; right: 0px; top: 0px;">
<button id="node-config-input-scan-groups" type="button" class="red-ui-button" style="margin-left: 10px;">
<i class="fa fa-search"></i>
</a>
</button>
</div>
</div>
<div class="form-row">
<label for="node-input-colornamer"><i class="fa fa-check-circle"></i> <span data-i18n="hue-group.config.colornamer"></span></label>
<label for="node-input-colornamer" style="width:70%">
<input type="checkbox" id="node-input-colornamer" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-group.config.colornamer-info"></span>
</label>
</div>
<div class="form-row">
<label for="node-input-skipevents"><i class="fa fa-sign-out"></i> <span data-i18n="hue-group.config.skipevents"></span></label>
<input type="checkbox" id="node-input-skipevents" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-group.config.skipevents-node"></span>
</label>
<div class="form-row" style="margin-top: 30px">
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-skipevents" style="flex: 15px;">
<span data-i18n="hue-group.config.skipevents-node" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
<div class="form-row">
<label for="node-input-universalevents"><i class="fa fa-bullhorn"></i> <span data-i18n="hue-group.config.universalevents"></span></label>
<input type="checkbox" id="node-input-universalevents" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-group.config.universalevents-node"></span>
</label>
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-initevents" style="flex: 15px;">
<span data-i18n="hue-group.config.sendinitevents-node" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
</script>

Expand All @@ -41,97 +37,172 @@
category: 'HueMagic',
color: '#f94f8b',
defaults: {
name: {value:""},
bridge: {type: "hue-bridge", required: true},
groupid: {value:"", required: false, validate: function(id) {
if(id.length < 1) { return true; }
else if(!isNaN(id)) { return true; }
else { return false; }
}},
colornamer: {value: true},
skipevents: {value: false},
universalevents: {value: false}
name: { value:"" },
bridge: { type: "hue-bridge", required: true },
groupid: { value:"", required: false },
skipevents: { value: false },
initevents: { value: false }
},
align: 'left',
icon: "hue-group.png",
inputs: 1,
outputs: 1,
label: function() {
return this.name || this._("hue-group.node.title");
},
paletteLabel: function() {
return this._("hue-group.node.title");
},
inputLabels: function() {
return this._("hue-group.node.input");
},
outputLabels: function() {
return this._("hue-group.node.output");
},
align: 'right',
icon: "hue-group.png",
paletteLabel: function() {
return this._("hue-group.node.title");
},
label: function() {
return this.name || this._("hue-group.node.title");
},
oneditprepare: function()
{
var scope = this;
const scope = this;
let options = [];

function manualGroupID()
function manualInput()
{
// GET CURRENT SELECTED VALUE
var current = $('#node-input-groupid').val();
$('#node-input-groupid').replaceWith('<input type="text" id="node-input-groupid" style="width: 100%"/>');
$('#node-input-groupid').val(current);

// REMOVE SELECT FIELD
$('#input-select-toggle').empty();

// CREATE NEW INPUT FIELD
$('#input-select-toggle').append('<input type="text" id="node-input-groupid" placeholder="00000000-0000-0000-0000-000000000000" style="width: 100%" value="'+current+'" />');

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-groups");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-pencil");
buttonIcon.addClass("fa-search");
}

function searchAndSelectGroupID()
function searchAndSelect()
{
// GET CURRENT BRIDGE CONFIGURATION
var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
if(!bridgeConfig) { return false; }

// GET CURRENT SELECTED VALUE
var current = $('#node-input-groupid').val();
$('#node-input-groupid').replaceWith('<select id="node-input-groupid" style="width: 100%"></select>');
$('#node-input-groupid').append('<option selected="selected" value="null">'+scope._("hue-group.config.searching")+'</option>');

var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
$.get('hue/groups', {bridge: bridgeConfig.bridge, key: bridgeConfig.key})
.done(function(data) {
var groups = JSON.parse(data);
// TRIGGER SEARCHING NOTIFICATION
var notification = RED.notify(scope._("hue-group.config.searching"), { type: "compact", modal: true, fixed: true });

if(groups.length <= 0)
// GET THE GROUPS
$.get('hue/ressources', { bridge: bridgeConfig.bridge, key: bridgeConfig.key, type: "group" })
.done( function(data) {
var allRessources = JSON.parse(data);
if(allRessources.length <= 0)
{
RED.notify(scope._("hue-group.config.none-found"), "error");
notification.close();
RED.notify(scope._("hue-group.config.none-found"), { type: "error" });
return false;
}

// RESET OPTIONS
$('#node-input-groupid').empty();

// SET GROUPS AS OPTIONS
$('#node-input-groupid').append('<option value="0">'+scope._("hue-group.config.all")+'</option>');
groups.forEach(function(group)
allRessources.forEach(function(ressource)
{
$('#node-input-groupid').append('<option value="' + group.id + '">' + group.name + '</option>');
if(!ressource.name) // ALL SPECIAL
{
options[ressource.id] = { value: ressource.id, label: "â—Ź "+scope._("hue-group.config.all") };
}
});

allRessources.forEach(function(ressource)
{
if(ressource.name) // REGULAR GROUPS
{
if(ressource.model)
{
options[ressource.id] = { value: ressource.id, label: ressource.name + " ("+ressource.model+")" };
}
else
{
options[ressource.id] = { value: ressource.id, label: ressource.name };
}
}
});

// SELECT CURRENT VALUE
$('#node-input-groupid').val(current);
$("#node-input-groupid").typedInput({
types: [
{
value: current,
options: Object.values(options)
}
]
});

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-groups");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-search");
buttonIcon.addClass("fa-pencil");

// CLOSE SEARCH NOTIFICATION
notification.close();
})
.fail(function()
{
notification.close();
RED.notify(scope._("hue-group.config.unknown-error"), "error");
});
}

$(document).on('change', '#node-input-groupid', function()
// CHANGED GROUP ID? -> REPLACE NAME (IF POSSIBLE)
$(document).on('change', '#node-input-groupid', function(e)
{
var groupName = $('#node-input-groupid option:selected').text();
if(groupName.length > 0)
let currentSelectedOptionID = $(e.currentTarget).val();
let currentSelectedOptionValue = (currentSelectedOptionID.length > 0 && options[currentSelectedOptionID]) ? options[currentSelectedOptionID].label : false;

if(currentSelectedOptionValue !== false)
{
$('#node-input-name').val(groupName);
$('#node-input-name').val(currentSelectedOptionValue.split(" (")[0]);
}
});

// TOGGLE SELECT/INPUT FIELD
$('#node-config-input-scan-groups').click(function()
{
if($('#node-input-groupid').prop("tagName") === "INPUT")
if($('#input-select-toggle').find(".red-ui-typedInput-container").length > 0)
{
searchAndSelectGroupID();
} else {
manualGroupID();
manualInput();
}
else
{
searchAndSelect();
}
});
},
button: {
enabled: function() {
return (this.groupid && this.groupid.length > 1)
},
visible: function() {
return (this.groupid && this.groupid.length > 1)
},
onclick: function()
{
const node = this;
if(node.bridge)
{
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify({ __user_inject_props__: "status"}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node.name + ": " + node._("hue-group.node.statusmsg"), { type: "success", id: "status", timeout: 2000 });
}
});
}
}
}
});
</script>
790 changes: 387 additions & 403 deletions huemagic/hue-group.js

Large diffs are not rendered by default.

196 changes: 132 additions & 64 deletions huemagic/hue-light.html
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
<script type="text/x-red" data-template-name="hue-light">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="hue-light.config.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-light.config.input-name">
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-light.config.input-name" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-bridge"><i class="fa fa-server"></i> Bridge</label>
<input type="text" id="node-input-bridge">
<input type="text" id="node-input-bridge" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-lightid"><i class="fa fa-lightbulb-o"></i> <span data-i18n="hue-light.config.light"></span></label>
<div style="display: inline-block; position: relative; width: 70%; height: 20px;">
<div style="position: absolute; left: 0px; right: 40px;">
<input type="text" id="node-input-lightid" placeholder="1" style="width: 100%"/>
<div style="display: inline-flex; width: calc(100% - 105px)">
<div id="input-select-toggle" style="flex-grow: 1;">
<input type="text" id="node-input-lightid" placeholder="00000000-0000-0000-0000-000000000000" style="width: 100%"/>
</div>
<a id="node-config-input-scan-lights" class="editor-button" style="position: absolute; right: 0px; top: 0px;">
<button id="node-config-input-scan-lights" type="button" class="red-ui-button" style="margin-left: 10px;">
<i class="fa fa-search"></i>
</a>
</button>
</div>
</div>
<div class="form-row">
<label for="node-input-colornamer"><i class="fa fa-check-circle"></i> <span data-i18n="hue-light.config.colornamer"></span></label>
<label for="node-input-colornamer" style="width:70%">
<input type="checkbox" id="node-input-colornamer" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-light.config.colornamer-info"></span>
</label>
<div class="form-row" style="margin-top: 30px">
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-colornamer" style="flex: 15px;">
<span data-i18n="hue-light.config.colornamer-info" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
<div class="form-row">
<label for="node-input-skipevents"><i class="fa fa-sign-out"></i> <span data-i18n="hue-light.config.skipevents"></span></label>
<input type="checkbox" id="node-input-skipevents" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-light.config.skipevents-node"></span>
</label>
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-skipevents" style="flex: 15px;">
<span data-i18n="hue-light.config.skipevents-node" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
<div class="form-row">
<label for="node-input-universalevents"><i class="fa fa-bullhorn"></i> <span data-i18n="hue-light.config.universalevents"></span></label>
<input type="checkbox" id="node-input-universalevents" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-light.config.universalevents-node"></span>
</label>
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-initevents" style="flex: 15px;">
<span data-i18n="hue-light.config.sendinitevents-node" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
</script>

Expand All @@ -41,96 +43,162 @@
category: 'HueMagic',
color: '#47c0e8',
defaults: {
name: {value:""},
bridge: {type: "hue-bridge", required: true},
lightid: {value:"", required: false, validate: function(id) {
if(id.length < 1) { return true; }
else if(!isNaN(id)) { return true; }
else { return false; }
}},
colornamer: {value: true},
skipevents: {value: false},
universalevents: {value: false}
name: { value:"" },
bridge: { type: "hue-bridge", required: true },
lightid: { value:"", required: false },
colornamer: { value: true },
skipevents: { value: false },
initevents: { value: false }
},
align: 'left',
icon: "hue-light.png",
inputs: 1,
outputs: 1,
label: function() {
return this.name || this._("hue-light.node.title");
},
paletteLabel: function() {
return this._("hue-light.node.title");
},
inputLabels: function() {
return this._("hue-light.node.input");
},
outputLabels: function() {
return this._("hue-light.node.output");
},
align: 'right',
icon: "hue-light.png",
paletteLabel: function() {
return this._("hue-light.node.title");
},
label: function() {
return this.name || this._("hue-light.node.title");
},
oneditprepare: function()
{
var scope = this;
const scope = this;
let options = [];

function manualLightID()
function manualInput()
{
// GET CURRENT SELECTED VALUE
var current = $('#node-input-lightid').val();
$('#node-input-lightid').replaceWith('<input type="text" id="node-input-lightid" style="width: 100%"/>');
$('#node-input-lightid').val(current);

// REMOVE SELECT FIELD
$('#input-select-toggle').empty();

// CREATE NEW INPUT FIELD
$('#input-select-toggle').append('<input type="text" id="node-input-lightid" placeholder="00000000-0000-0000-0000-000000000000" style="width: 100%" value="'+current+'" />');

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-lights");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-pencil");
buttonIcon.addClass("fa-search");
}

function searchAndSelectLightID()
function searchAndSelect()
{
// GET CURRENT BRIDGE CONFIGURATION
var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
if(!bridgeConfig) { return false; }

// GET CURRENT SELECTED VALUE
var current = $('#node-input-lightid').val();
$('#node-input-lightid').replaceWith('<select id="node-input-lightid" style="width: 100%"></select>');
$('#node-input-lightid').append('<option selected="selected" value="null">'+scope._("hue-light.config.searching")+'</option>');

var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
$.get('hue/lights', {bridge: bridgeConfig.bridge, key: bridgeConfig.key, type: "ZLLPresence"})
.done(function(data) {
var lightBulbs = JSON.parse(data);
// TRIGGER SEARCHING NOTIFICATION
var notification = RED.notify(scope._("hue-light.config.searching"), { type: "compact", modal: true, fixed: true });

if(lightBulbs.length <= 0)
// GET THE LIGHTS
$.get('hue/ressources', { bridge: bridgeConfig.bridge, key: bridgeConfig.key, type: "light" })

This comment has been minimized.

Copy link
@dhlavaty

dhlavaty Jan 9, 2022

Is it really ressources ? Isn't it a typo ?

.done( function(data) {
var allRessources = JSON.parse(data);
if(allRessources.length <= 0)
{
RED.notify(scope._("hue-light.config.none-found"), "error");
notification.close();
RED.notify(scope._("hue-light.config.none-found"), { type: "error" });
return false;
}

// RESET OPTIONS
$('#node-input-lightid').empty();

// SET LIGHTS AS OPTIONS
lightBulbs.forEach(function(light)
// SET OPTIONS
allRessources.forEach(function(ressource)
{
$('#node-input-lightid').append('<option value="' + light.id + '">' + light.name + '</option>');
if(ressource.model)
{
options[ressource.id] = { value: ressource.id, label: ressource.name + " ("+ressource.model+")" };
}
else
{
options[ressource.id] = { value: ressource.id, label: ressource.name };
}
});

// SELECT CURRENT VALUE
$('#node-input-lightid').val(current);
$("#node-input-lightid").typedInput({
types: [
{
value: current,
options: Object.values(options)
}
]
});

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-lights");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-search");
buttonIcon.addClass("fa-pencil");

// CLOSE SEARCH NOTIFICATION
notification.close();
})
.fail(function()
{
notification.close();
RED.notify(scope._("hue-light.config.unknown-error"), "error");
});
}

$(document).on('change', '#node-input-lightid', function()
// CHANGED LIGHT ID? -> REPLACE NAME (IF POSSIBLE)
$(document).on('change', '#node-input-lightid', function(e)
{
var lightName = $('#node-input-lightid option:selected').text();
if(lightName.length > 0)
let currentSelectedOptionID = $(e.currentTarget).val();
let currentSelectedOptionValue = (currentSelectedOptionID.length > 0 && options[currentSelectedOptionID]) ? options[currentSelectedOptionID].label : false;

if(currentSelectedOptionValue !== false)
{
$('#node-input-name').val(lightName);
$('#node-input-name').val(currentSelectedOptionValue.split(" (")[0]);
}
});

// TOGGLE SELECT/INPUT FIELD
$('#node-config-input-scan-lights').click(function()
{
if($('#node-input-lightid').prop("tagName") === "INPUT")
if($('#input-select-toggle').find(".red-ui-typedInput-container").length > 0)
{
manualInput();
}
else
{
searchAndSelectLightID();
} else {
manualLightID();
searchAndSelect();
}
});
},
button: {
enabled: function() {
return (this.lightid && this.lightid.length > 1)
},
visible: function() {
return (this.lightid && this.lightid.length > 1)
},
onclick: function()
{
const node = this;
if(node.bridge)
{
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify({ __user_inject_props__: "status"}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node.name + ": " + node._("hue-light.node.statusmsg"), { type: "success", id: "status", timeout: 2000 });
}
});
}
}
}
});
</script>
922 changes: 544 additions & 378 deletions huemagic/hue-light.js

Large diffs are not rendered by default.

56 changes: 36 additions & 20 deletions huemagic/hue-magic.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script type="text/x-red" data-template-name="hue-magic">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="hue-magic.config.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-magic.config.input-name">
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-magic.config.input-name" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-endless"><i class="fa fa-refresh"></i> <span data-i18n="hue-magic.config.loop"></span></label>
Expand All @@ -13,15 +13,15 @@
<input type="checkbox" id="node-input-restore" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-magic.config.restore-state"></span>
</label>
</div>
<div class="form-row">
<div class="form-row" style="margin-top: 30px">
<label for="node-input-steps"><i class="fa fa-magic"></i> <span data-i18n="hue-magic.config.animation"></span></label>
</div>
<div style="display: none;">
<div class="form-row">
<input type="text" id="node-input-preset" placeholder="...">
<input type="text" id="node-input-preset" placeholder="{}">
</div>
<div class="form-row">
<input type="text" id="node-input-steps" placeholder="...">
<input type="text" id="node-input-steps" placeholder="{}">
</div>
</div>
<div class="form-row form-animations">
Expand Down Expand Up @@ -145,31 +145,31 @@
category: 'HueMagic',
color: '#ff4242',
defaults: {
name: {value:""},
endless: {value: true},
restore: {value: false},
preset: {value: null},
steps: {value: null}
name: { value:"" },
endless: { value: true },
restore: { value: false },
preset: { value: null },
steps: { value: null }
},
align: 'left',
icon: "hue-magic.png",
inputs: 1,
outputs: 1,
label: function() {
return this.name || this._("hue-magic.node.title");
},
paletteLabel: function() {
return this._("hue-magic.node.title");
},
inputLabels: function() {
return this._("hue-magic.node.input");
},
outputLabels: function() {
return this._("hue-magic.node.output");
},
align: 'right',
icon: "hue-magic.png",
paletteLabel: function() {
return this._("hue-magic.node.title");
},
label: function() {
return this.name || this._("hue-magic.node.title");
},
oneditprepare: function()
{
var scope = this;
const scope = this;
this.animations = [];

var preset = $('#node-input-preset').val();
Expand All @@ -178,7 +178,6 @@
$.get('hue/animations?nc=' + Math.random())
.done( function(data)
{
// LOGIC
scope.animations = JSON.parse(data);
$.each(scope.animations, function(i, animation)
{
Expand Down Expand Up @@ -238,9 +237,26 @@
// SET STEPS
var index = selectedAnimation.attr("data-id");
var steps = JSON.stringify(JSON.parse(selectedAnimation.attr("data-steps")));

$('#node-input-steps').val(steps);
});
},
button: {
onclick: function()
{
const node = this;
if(node.steps)
{
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify({ __user_inject_props__: "play"}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node.name + ": " + node._("hue-magic.node.playmsg"), { type: "success", id: "status", timeout: 2000 });
}
});
}
}
}
});
</script>
70 changes: 43 additions & 27 deletions huemagic/hue-magic.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ module.exports = function(RED)
{
RED.nodes.createNode(this, config);

var scope = this;
let async = require('async');
var isEndless = config.endless;
var restoreState = config.restore;
const scope = this;
const async = require('async');
const isEndless = config.endless;
const restoreState = config.restore;

// STEPS INITIALIZATION
this.steps = config.steps;
Expand Down Expand Up @@ -182,22 +182,25 @@ module.exports = function(RED)
}

//
// ENABLE HUE MAGIC ANIMATION
// START THE HUEMAGIC ANIMATION
this.on('input', function(msg, send, done)
{
// Node-RED < 1.0
// REDEFINE SEND AND DONE IF NOT AVAILABLE
send = send || function() { scope.send.apply(scope,arguments); }
done = done || function() { scope.done.apply(scope,arguments); }

if(typeof msg.payload.steps != 'undefined') {
if(typeof msg.payload != 'undefined' && typeof msg.payload.steps != 'undefined')
{
scope.steps = msg.payload.steps;
//we animate if we receive steps from the input.
msg.payload.animate = true;
}

const playFromButton = (typeof msg.__user_inject_props__ != 'undefined' && msg.__user_inject_props__ == "play");

if(scope.steps != null)
{
// SPECIALS CONFIG
if(typeof msg.payload.specials != 'undefined')
if(typeof msg.payload != 'undefined' && typeof msg.payload.specials != 'undefined')
{
// APPLY RANDOM ORDER CONFIG
if(typeof msg.payload.specials.randomOrder != 'undefined')
Expand All @@ -207,7 +210,7 @@ module.exports = function(RED)
}

// TURN ON ANIMATION
if(msg.payload.animate == true||msg.payload === true)
if(typeof msg.payload != 'undefined' && msg.payload.animate == true||msg.payload === true||playFromButton === true)
{
var animationSteps = typeof scope.steps === 'string' ? JSON.parse(scope.steps) : scope.steps;
if(scope.isAnimating == false)
Expand All @@ -220,7 +223,7 @@ module.exports = function(RED)
}

// TURN OFF ANIMATION
if((typeof msg.payload.animate != 'undefined' && msg.payload.animate == false)||msg.payload === false)
if((typeof msg.payload != 'undefined' && typeof msg.payload.animate != 'undefined' && msg.payload.animate == false)||msg.payload === false)
{
scope.animationStopped(done);
scope.isAnimating = false;
Expand Down Expand Up @@ -250,25 +253,32 @@ module.exports = function(RED)
// GET ANIMATIONS
RED.httpAdmin.get('/hue/animations', function(req, res, next)
{
let fs = require("fs");
let path = require("path");
const fs = require("fs");
const path = require("path");
const dir = path.resolve(__dirname, 'animations');

var allAnimations = [];
var dir = path.resolve(__dirname, 'animations');
fs.readdirSync(dir).forEach(function(filename)
{
try
{
var filepath = path.resolve(dir, filename);
var stat = fs.statSync(filepath);
var isFile = stat.isFile();
var fileID = path.basename(filepath, '.json');

fs.readdirSync(dir).forEach(filename => {
var filepath = path.resolve(dir, filename);
var stat = fs.statSync(filepath);
var isFile = stat.isFile();
var fileID = path.basename(filepath, '.json');
if(isFile)
{
var animation = JSON.parse(fs.readFileSync(filepath, "utf8"));
animation.info.id = fileID;

if(isFile)
allAnimations.push(animation);
};
}
catch(e)
{
var animation = JSON.parse(fs.readFileSync(filepath, "utf8"));
animation.info.id = fileID;

allAnimations.push(animation);
};
console.log(fileID, e);
}
});

// SEND ALL ANIMATIONS
Expand All @@ -280,14 +290,20 @@ module.exports = function(RED)
RED.httpAdmin.get('/hue/animations/:file', function(req, res, next)
{
let path = require("path");
res.sendFile(path.resolve(__dirname, 'animations', 'previews', req.params.file));
res.sendFile(req.params.file, {
root: path.resolve(__dirname, 'animations', 'previews'),
dotfiles: 'deny'
});
});

//
// GET ASSETS
RED.httpAdmin.get('/hue/assets/:file', function(req, res, next)
{
let path = require("path");
res.sendFile(path.resolve(__dirname, 'assets', req.params.file));
res.sendFile(req.params.file, {
root: path.resolve(__dirname, 'assets'),
dotfiles: 'deny'
});
});
}
186 changes: 127 additions & 59 deletions huemagic/hue-motion.html
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
<script type="text/x-red" data-template-name="hue-motion">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="hue-motion.config.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-motion.config.input-name">
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-motion.config.input-name" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-bridge"><i class="fa fa-server"></i> Bridge</label>
<input type="text" id="node-input-bridge">
<input type="text" id="node-input-bridge" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-sensorid"><i class="fa fa-eye"></i> <span data-i18n="hue-motion.config.sensor"></span></label>
<div style="display: inline-block; position: relative; width: 70%; height: 20px;">
<div style="position: absolute; left: 0px; right: 40px;">
<input type="text" id="node-input-sensorid" placeholder="1" style="width: 100%"/>
<div style="display: inline-flex; width: calc(100% - 105px)">
<div id="input-select-toggle" style="flex-grow: 1;">
<input type="text" id="node-input-sensorid" placeholder="00000000-0000-0000-0000-000000000000" style="width: 100%"/>
</div>
<a id="node-config-input-scan-sensors" class="editor-button" style="position: absolute; right: 0px; top: 0px;">
<button id="node-config-input-scan-sensors" type="button" class="red-ui-button" style="margin-left: 10px;">
<i class="fa fa-search"></i>
</a>
</button>
</div>
</div>
<div class="form-row">
<label for="node-input-skipevents"><i class="fa fa-sign-out"></i> <span data-i18n="hue-motion.config.skipevents"></span></label>
<input type="checkbox" id="node-input-skipevents" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-motion.config.skipevents-node"></span>
</label>
<div class="form-row" style="margin-top: 30px">
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-skipevents" style="flex: 15px;">
<span data-i18n="hue-motion.config.skipevents-node" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
<div class="form-row">
<label for="node-input-universalevents"><i class="fa fa-bullhorn"></i> <span data-i18n="hue-motion.config.universalevents"></span></label>
<input type="checkbox" id="node-input-universalevents" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-motion.config.universalevents-node"></span>
</label>
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-initevents" style="flex: 15px;">
<span data-i18n="hue-motion.config.sendinitevents-node" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
</script>

Expand All @@ -35,95 +37,161 @@
category: 'HueMagic',
color: '#9876d3',
defaults: {
name: {value:""},
bridge: {type: "hue-bridge", required: true},
sensorid: {value:"", required: false, validate: function(id) {
if(id.length < 1) { return true; }
else if(!isNaN(id)) { return true; }
else { return false; }
}},
skipevents: {value: false},
universalevents: {value: false}
name: { value:"" },
bridge: { type: "hue-bridge", required: true },
sensorid: { value:"", required: false },
skipevents: { value: false },
initevents: { value: false }
},
align: 'left',
icon: "hue-motion.png",
inputs: 1,
outputs: 1,
label: function() {
return this.name || this._("hue-motion.node.title");
},
paletteLabel: function() {
return this._("hue-motion.node.title");
},
inputLabels: function() {
return this._("hue-motion.node.input");
},
outputLabels: function() {
return this._("hue-motion.node.output");
},
align: 'right',
icon: "bridge.png",
paletteLabel: function() {
return this._("hue-motion.node.title");
},
label: function() {
return this.name || this._("hue-motion.node.title");
},
oneditprepare: function()
{
var scope = this;
const scope = this;
let options = [];

function manualMotionSensorID()
function manualInput()
{
// GET CURRENT SELECTED VALUE
var current = $('#node-input-sensorid').val();
$('#node-input-sensorid').replaceWith('<input type="text" id="node-input-sensorid" style="width: 100%"/>');
$('#node-input-sensorid').val(current);

// REMOVE SELECT FIELD
$('#input-select-toggle').empty();

// CREATE NEW INPUT FIELD
$('#input-select-toggle').append('<input type="text" id="node-input-sensorid" placeholder="00000000-0000-0000-0000-000000000000" style="width: 100%" value="'+current+'" />');

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-sensors");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-pencil");
buttonIcon.addClass("fa-search");
}

function searchAndSelectMotionSensor()
function searchAndSelect()
{
// GET CURRENT BRIDGE CONFIGURATION
var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
if(!bridgeConfig) { return false; }

// GET CURRENT SELECTED VALUE
var current = $('#node-input-sensorid').val();
$('#node-input-sensorid').replaceWith('<select id="node-input-sensorid" style="width: 100%"></select>');
$('#node-input-sensorid').append('<option selected="selected" value="null">'+scope._("hue-motion.config.searching")+'</option>');

var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
$.get('hue/sensors', {bridge: bridgeConfig.bridge, key: bridgeConfig.key, type: "ZLLPresence"})
.done(function(data) {
var motionSensors = JSON.parse(data);
// TRIGGER SEARCHING NOTIFICATION
var notification = RED.notify(scope._("hue-motion.config.searching"), { type: "compact", modal: true, fixed: true });

if(motionSensors.length <= 0)
// GET THE SENSORS
$.get('hue/ressources', { bridge: bridgeConfig.bridge, key: bridgeConfig.key, type: "motion" })
.done( function(data) {
var allRessources = JSON.parse(data);
if(allRessources.length <= 0)
{
RED.notify(scope._("hue-motion.config.none-found"), "error");
notification.close();
RED.notify(scope._("hue-motion.config.none-found"), { type: "error" });
return false;
}

// RESET OPTIONS
$('#node-input-sensorid').empty();

// SET MOTION SENSORS AS OPTIONS
motionSensors.forEach(function(sensor)
// SET OPTIONS
allRessources.forEach(function(ressource)
{
$('#node-input-sensorid').append('<option value="' + sensor.id + '">' + sensor.name + '</option>');
if(ressource.model)
{
options[ressource.id] = { value: ressource.id, label: ressource.name + " ("+ressource.model+")" };
}
else
{
options[ressource.id] = { value: ressource.id, label: ressource.name };
}
});

// SELECT CURRENT VALUE
$('#node-input-sensorid').val(current);
$("#node-input-sensorid").typedInput({
types: [
{
value: current,
options: Object.values(options)
}
]
});

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-sensors");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-search");
buttonIcon.addClass("fa-pencil");

// CLOSE SEARCH NOTIFICATION
notification.close();
})
.fail(function()
{
notification.close();
RED.notify(scope._("hue-motion.config.unknown-error"), "error");
});
}

$(document).on('change', '#node-input-sensorid', function()
// CHANGED SENSOR ID? -> REPLACE NAME (IF POSSIBLE)
$(document).on('change', '#node-input-sensorid', function(e)
{
var sensorName = $('#node-input-sensorid option:selected').text();
if(sensorName.length > 0)
let currentSelectedOptionID = $(e.currentTarget).val();
let currentSelectedOptionValue = (currentSelectedOptionID.length > 0 && options[currentSelectedOptionID]) ? options[currentSelectedOptionID].label : false;

if(currentSelectedOptionValue !== false)
{
$('#node-input-name').val(sensorName);
$('#node-input-name').val(currentSelectedOptionValue.split(" (")[0]);
}
});

// TOGGLE SELECT/INPUT FIELD
$('#node-config-input-scan-sensors').click(function()
{
if($('#node-input-sensorid').prop("tagName") === "INPUT")
if($('#input-select-toggle').find(".red-ui-typedInput-container").length > 0)
{
searchAndSelectMotionSensor();
} else {
manualMotionSensorID();
manualInput();
}
else
{
searchAndSelect();
}
});
},
button: {
enabled: function() {
return (this.sensorid && this.sensorid.length > 1)
},
visible: function() {
return (this.sensorid && this.sensorid.length > 1)
},
onclick: function()
{
const node = this;
if(node.bridge)
{
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify({ __user_inject_props__: "status"}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node.name + ": " + node._("hue-motion.node.statusmsg"), { type: "success", id: "status", timeout: 2000 });
}
});
}
}
}
});
</script>
200 changes: 99 additions & 101 deletions huemagic/hue-motion.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ module.exports = function(RED)
function HueMotion(config)
{
RED.nodes.createNode(this, config);
var scope = this;
let bridge = RED.nodes.getNode(config.bridge);
let { HueMotionMessage } = require('../utils/messages');
var universalMode = false;

// SAVE LAST STATE
var lastState = false;
const scope = this;
const bridge = RED.nodes.getNode(config.bridge);

// SAVE LAST COMMAND
this.lastCommand = null;

//
// CHECK CONFIG
Expand All @@ -25,7 +24,6 @@ module.exports = function(RED)
// UNIVERSAL MODE?
if(!config.sensorid)
{
universalMode = true;
this.status({fill: "grey", shape: "dot", text: "hue-motion.node.universal"});
}

Expand All @@ -37,131 +35,131 @@ module.exports = function(RED)
}

//
// ON UPDATE
if(config.sensorid) { bridge.events.on('sensor' + config.sensorid, function(sensor) { scope.receivedUpdates(sensor) }); }
if(!config.sensorid && config.universalevents && config.universalevents == true) { bridge.events.on('sensor', function(sensor) { scope.receivedUpdates(sensor) }); }

//
// RECEIVED UPDATES
this.receivedUpdates = function(sensor)
// SUBSCRIBE TO UPDATES FROM THE BRIDGE
bridge.subscribe("motion", config.sensorid, function(info)
{
if(sensor.config.reachable == false)
let currentState = bridge.get("motion", info.id);

// RESSOURCE FOUND?
if(currentState !== false)
{
// SEND STATUS
if(universalMode == false)
// SEND MESSAGE
if(!config.skipevents && (config.initevents || info.suppressMessage == false))
{
scope.status({fill: "red", shape: "ring", text: "hue-motion.node.not-reachable"});
// SET LAST COMMAND
if(scope.lastCommand !== null)
{
currentState.command = scope.lastCommand;
}

// SEND STATE
scope.send(currentState);

// RESET LAST COMMAND
scope.lastCommand = null;
}
}
else if(sensor.config.on == true)
{
// SEND STATUS
if(universalMode == false)

// NOT IN UNIVERAL MODE? -> CHANGE UI STATES
if(config.sensorid)
{
if(sensor.state.presence)
if(currentState.payload.reachable == false)
{
scope.status({fill: "red", shape: "ring", text: "hue-motion.node.not-reachable"});
}
else if(currentState.payload.active == true)
{
scope.status({fill: "green", shape: "dot", text: "hue-motion.node.motion"});
if(currentState.payload.motion)
{
scope.status({fill: "green", shape: "dot", text: "hue-motion.node.motion"});
}
else
{
scope.status({fill: "grey", shape: "dot", text: "hue-motion.node.activated"});
}
}
else
else if(currentState.payload.active == false)
{
scope.status({fill: "grey", shape: "dot", text: "hue-motion.node.activated"});
scope.status({fill: "red", shape: "ring", text: "hue-motion.node.deactivated"});
}
}

// SEND MESSAGE
var hueMotion = new HueMotionMessage(sensor, true, (universalMode == false) ? lastState : false);
if(!config.skipevents) { scope.send(hueMotion.msg); }

// SAVE LAST STATE
lastState = sensor;
}
else if(sensor.config.on == false)
{
// SEND STATUS
if(universalMode == false)
{
scope.status({fill: "red", shape: "ring", text: "hue-motion.node.deactivated"});
}

// SEND MESSAGE
var hueMotion = new HueMotionMessage(sensor, false, (universalMode == false) ? lastState : false);
if(!config.skipevents) { scope.send(hueMotion.msg); }

// SAVE LAST STATE
lastState = sensor;
}
}
});

//
// DISABLE / ENABLE SENSOR
// CONTROL SENSOR
this.on('input', function(msg, send, done)
{
// Node-RED < 1.0
// REDEFINE SEND AND DONE IF NOT AVAILABLE
send = send || function() { scope.send.apply(scope,arguments); }
done = done || function() { scope.done.apply(scope,arguments); }

// SAVE LAST COMMAND
scope.lastCommand = msg;

// DEFINE SENSOR ID
var tempSensorID = (msg.topic != null && isNaN(msg.topic) == false && msg.topic.length > 0) ? parseInt(msg.topic) : config.sensorid;
// CREATE PATCH
let patchObject = {};

// DEFINE SENSOR ID & CURRENT STATE
const tempSensorID = (msg.topic != null) ? msg.topic : config.sensorid;
let currentState = bridge.get("motion", tempSensorID);

// GET CURRENT STATE
if(typeof msg.payload != 'undefined' && typeof msg.payload.status != 'undefined')
if( (typeof msg.payload != 'undefined' && typeof msg.payload.status != 'undefined') || (typeof msg.__user_inject_props__ != 'undefined' && msg.__user_inject_props__ == "status") )
{
bridge.client.sensors.getById(tempSensorID)
.then(sensor => {
var hueMotion = new HueMotionMessage(sensor, (sensor.config.on) ? true : false, (universalMode == false) ? lastState : false);
// SET LAST COMMAND
if(scope.lastCommand !== null)
{
currentState.command = scope.lastCommand;
}

// SAVE LAST STATE
lastState = sensor;
// SEND STATE
scope.send(currentState);

return send(hueMotion.msg);
});
// RESET LAST COMMAND
scope.lastCommand = null;

if(done) { done(); }
return true;
}

// CONTROL
if(msg.payload == true || msg.payload == false)
// TURN ON / OFF
if((msg.payload === true || msg.payload === false) && (msg.payload !== currentState.payload.active))
{
bridge.client.sensors.getById(tempSensorID)
.then(sensor => {
sensor.config.on = msg.payload;
return bridge.client.sensors.save(sensor);
})
.then(sensor => {
var hueMotion = new HueMotionMessage(sensor, msg.payload, lastState);

// SEND STATUS
if(universalMode == false)
{
if(msg.payload == false)
{
scope.status({fill: "red", shape: "ring", text: "hue-motion.node.deactivated"});
}
else
{
scope.status({fill: "green", shape: "dot", text: "hue-motion.node.activated"});
}
}
// PREPARE PATCH
patchObject.enabled = msg.payload;
}

// SEND MESSAGE
if(!config.skipevents) { send(hueMotion.msg); }
if(done) { done(); }

// SAVE LAST STATE
lastState = sensor;
})
.catch(error => {
scope.error(error, msg);
if(done) { done(error); }
});
//
// SHOULD PATCH?
if(Object.values(patchObject).length > 0)
{
// CHANGE NODE UI STATE
if(config.sensorid)
{
scope.status({fill: "grey", shape: "ring", text: "hue-motion.node.command"});
}

// PATCH!
bridge.patch("motion", tempSensorID, patchObject)
.then(function() { if(done) { done(); }})
.catch(function(errors) { scope.error(errors); });
}
});
else
{
// SET LAST COMMAND
if(scope.lastCommand !== null)
{
currentState.command = scope.lastCommand;
}

// SEND STATE
scope.send(currentState);

//
// CLOSE NODE / REMOVE EVENT LISTENER
this.on('close', function()
{
bridge.events.removeAllListeners('sensor' + config.sensorid);
// RESET LAST COMMAND
scope.lastCommand = null;

if(done) { done(); }
}
});
}

Expand Down
171 changes: 121 additions & 50 deletions huemagic/hue-rules.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
<script type="text/x-red" data-template-name="hue-rules">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="hue-rules.config.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-rules.config.input-name">
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-rules.config.input-name" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-bridge"><i class="fa fa-server"></i> Bridge</label>
<input type="text" id="node-input-bridge">
<input type="text" id="node-input-bridge" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-ruleid"><i class="fa fa-tasks"></i> <span data-i18n="hue-rules.config.rule"></span></label>
<div style="display: inline-block; position: relative; width: 70%; height: 20px;">
<div style="position: absolute; left: 0px; right: 40px;">
<input type="text" id="node-input-ruleid" placeholder="1" style="width: 100%"/>
<div style="display: inline-flex; width: calc(100% - 105px)">
<div id="input-select-toggle" style="flex-grow: 1;">
<input type="text" id="node-input-ruleid" placeholder="0" style="width: 100%"/>
</div>
<a id="node-config-input-scan-rules" class="editor-button" style="position: absolute; right: 0px; top: 0px;">
<button id="node-config-input-scan-rules" type="button" class="red-ui-button" style="margin-left: 10px;">
<i class="fa fa-search"></i>
</a>
</button>
</div>
</div>
<div class="form-row" style="margin-top: 30px">
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-skipevents" style="flex: 15px;">
<span data-i18n="hue-rules.config.skipevents-node" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
<div class="form-row">
<label for="node-input-skipevents"><i class="fa fa-sign-out"></i> <span data-i18n="hue-rules.config.skipevents"></span></label>
<input type="checkbox" id="node-input-skipevents" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-rules.config.skipevents-node"></span>
</label>
<div style="display: inline-flex; width: calc(100% - 105px)">
<input type="checkbox" id="node-input-initevents" style="flex: 15px;">
<span data-i18n="hue-rules.config.sendinitevents-node" style="width: 100%; margin-left: 10px;"></span>
</div>
</div>
</script>

Expand All @@ -30,90 +37,154 @@
category: 'HueMagic',
color: '#bddb37',
defaults: {
name: {value:""},
bridge: {type: "hue-bridge", required: true},
ruleid: {value:"", required: true, validate: RED.validators.number()},
skipevents: {value: false}
name: { value:"" },
bridge: { type: "hue-bridge", required: true },
ruleid: { value:"", required: false },
skipevents: { value: false },
initevents: { value: false }
},
align: 'left',
icon: "hue-rules.png",
inputs: 1,
outputs: 1,
label: function() {
return this.name || this._("hue-rules.node.title");
},
paletteLabel: function() {
return this._("hue-rules.node.title");
},
inputLabels: function() {
return this._("hue-rules.node.input");
},
outputLabels: function() {
return this._("hue-rules.node.output");
},
align: 'right',
icon: "hue-rules.png",
paletteLabel: function() {
return this._("hue-rules.node.title");
},
label: function() {
return this.name || this._("hue-rules.node.title");
},
oneditprepare: function()
{
var scope = this;
const scope = this;
let options = [];

function manualRuleID()
function manualInput()
{
// GET CURRENT SELECTED VALUE
var current = $('#node-input-ruleid').val();
$('#node-input-ruleid').replaceWith('<input type="text" id="node-input-ruleid" style="width: 100%"/>');
$('#node-input-ruleid').val(current);

// REMOVE SELECT FIELD
$('#input-select-toggle').empty();

// CREATE NEW INPUT FIELD
$('#input-select-toggle').append('<input type="text" id="node-input-ruleid" placeholder="0" style="width: 100%" value="'+current+'" />');

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-rules");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-pencil");
buttonIcon.addClass("fa-search");
}

function searchAndSelectRuleID()
function searchAndSelect()
{
// GET CURRENT BRIDGE CONFIGURATION
var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
if(!bridgeConfig) { return false; }

// GET CURRENT SELECTED VALUE
var current = $('#node-input-ruleid').val();
$('#node-input-ruleid').replaceWith('<select id="node-input-ruleid" style="width: 100%"></select>');
$('#node-input-ruleid').append('<option selected="selected" value="null">'+scope._("hue-rules.config.searching")+'</option>');

var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
$.get('hue/rules', {bridge: bridgeConfig.bridge, key: bridgeConfig.key})
.done( function(data) {
var rules = JSON.parse(data);
// TRIGGER SEARCHING NOTIFICATION
var notification = RED.notify(scope._("hue-rules.config.searching"), { type: "compact", modal: true, fixed: true });

if(rules.length <= 0)
// GET THE SENSORS
$.get('hue/ressources', { bridge: bridgeConfig.bridge, key: bridgeConfig.key, type: "rule" })
.done( function(data) {
var allRessources = JSON.parse(data);
if(allRessources.length <= 0)
{
RED.notify(scope._("hue-rules.config.none-found"), "error");
notification.close();
RED.notify(scope._("hue-rules.config.none-found"), { type: "error" });
return false;
}

// RESET OPTIONS
$('#node-input-ruleid').empty();

// SET TEMPERATURE SENSORS AS OPTIONS
rules.forEach(function(rule)
// SET OPTIONS
allRessources.forEach(function(ressource)
{
$('#node-input-ruleid').append('<option value="' + rule.id + '">' + rule.name + '</option>');
options[ressource.id] = { value: ressource.id, label: ressource.name };
});

// SELECT CURRENT VALUE
$('#node-input-ruleid').val(current);
$("#node-input-ruleid").typedInput({
types: [
{
value: current,
options: Object.values(options)
}
]
});

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-rules");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-search");
buttonIcon.addClass("fa-pencil");

// CLOSE SEARCH NOTIFICATION
notification.close();
})
.fail(function()
{
notification.close();
RED.notify(scope._("hue-rules.config.unknown-error"), "error");
});
}

$(document).on('change', '#node-input-ruleid', function()
// CHANGED RULE ID? -> REPLACE NAME (IF POSSIBLE)
$(document).on('change', '#node-input-ruleid', function(e)
{
var sensorName = $('#node-input-ruleid option:selected').text();
if(sensorName.length > 0)
let currentSelectedOptionID = $(e.currentTarget).val();
let currentSelectedOptionValue = (currentSelectedOptionID.length > 0 && options[currentSelectedOptionID]) ? options[currentSelectedOptionID].label : false;

if(currentSelectedOptionValue !== false)
{
$('#node-input-name').val(sensorName);
$('#node-input-name').val(currentSelectedOptionValue.split(" (")[0]);
}
});

// TOGGLE SELECT/INPUT FIELD
$('#node-config-input-scan-rules').click(function()
{
if($('#node-input-ruleid').prop("tagName") === "INPUT")
if($('#input-select-toggle').find(".red-ui-typedInput-container").length > 0)
{
manualInput();
}
else
{
searchAndSelectRuleID();
} else {
manualRuleID();
searchAndSelect();
}
});
},
button: {
enabled: function() {
return (this.ruleid && (this.ruleid == 0 || this.ruleid.length > 0 || this.ruleid > -1))
},
visible: function() {
return (this.ruleid && (this.ruleid == 0 || this.ruleid.length > 0 || this.ruleid > -1))
},
onclick: function()
{
const node = this;
if(node.bridge)
{
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify({ __user_inject_props__: "status"}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node.name + ": " + node._("hue-rules.node.statusmsg"), { type: "success", id: "status", timeout: 2000 });
}
});
}
}
}
});
</script>
146 changes: 83 additions & 63 deletions huemagic/hue-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,103 +6,123 @@ module.exports = function(RED)
{
RED.nodes.createNode(this, config);

var scope = this;
let bridge = RED.nodes.getNode(config.bridge);
let { HueRulesMessage } = require('../utils/messages');
const scope = this;
const bridge = RED.nodes.getNode(config.bridge);

// SAVE LAST STATE
var lastState = false;
// SAVE LAST COMMAND
this.lastCommand = null;

//
// CHECK CONFIG
if(!config.ruleid || bridge == null)
if(bridge == null)
{
this.status({fill: "red", shape: "ring", text: "hue-rules.node.no-rule"});
return false;
}

//
// UNIVERSAL MODE?
if(!config.ruleid)
{
this.status({fill: "grey", shape: "dot", text: "hue-rules.node.universal"});
}

//
// UPDATE STATE
if(typeof bridge.disableupdates != 'undefined'||bridge.disableupdates == false)
if(typeof bridge.disableupdates != 'undefined' || bridge.disableupdates == false)
{
this.status({fill: "grey", shape: "dot", text: "hue-rules.node.init"});
}

//
// ON UPDATE
bridge.events.on('rule' + config.ruleid, function(rule)
// SUBSCRIBE TO UPDATES FROM THE BRIDGE
bridge.subscribe("rule", config.ruleid, function(info)
{
// SEND STATUS
if(rule.status == "enabled")
{
scope.status({fill: "green", shape: "dot", text: "hue-rules.node.enabled"});
}
else
{
scope.status({fill: "red", shape: "ring", text: "hue-rules.node.disabled"});
}
let currentState = bridge.get("rule", info.id);

// SEND MESSAGE
if(!config.skipevents)
// RESSOURCE FOUND?
if(currentState !== false)
{
var hueRule = new HueRulesMessage(rule, lastState);
scope.send(hueRule.msg);
}
// NOT IN UNIVERAL MODE? -> CHANGE UI STATES
if(config.ruleid)
{
if(currentState.payload.enabled == true)
{
scope.status({fill: "green", shape: "dot", text: "hue-rules.node.enabled"});
}
else
{
scope.status({fill: "red", shape: "ring", text: "hue-rules.node.disabled"});
}
}

// SEND MESSAGE
if(!config.skipevents && (config.initevents || info.suppressMessage == false))
{
// SET LAST COMMAND
if(scope.lastCommand !== null)
{
currentState.command = scope.lastCommand;
}

// SEND STATE
scope.send(currentState);

// SAVE LAST STATE
lastState = rule;
// RESET LAST COMMAND
scope.lastCommand = null;
}
}
});


//
// DISABLE / ENABLE RULE
this.on('input', function(msg, send, done)
{
// Node-RED < 1.0
// REDEFINE SEND AND DONE IF NOT AVAILABLE
send = send || function() { scope.send.apply(scope,arguments); }
done = done || function() { scope.done.apply(scope,arguments); }

// SAVE LAST COMMAND
scope.lastCommand = msg;

// CREATE PATCH
let patchObject = {};

// CONTROL
if(msg.payload == true || msg.payload == false)
// DEFINE SENSOR ID & CURRENT STATE
const tempRuleID = (msg.topic != null) ? msg.topic : config.ruleid;
let currentState = bridge.get("rule", "rule_" + tempRuleID);

// CONTROL RULE
if(msg.payload === true || msg.payload === false)
{
bridge.client.rules.getById(config.ruleid)
.then(rule => {
rule.status = (msg.payload == true) ? 'enabled' : 'disabled';
return bridge.client.rules.save(rule);
})
.then(rule => {
// SEND STATUS
if(msg.payload == false)
{
scope.status({fill: "red", shape: "ring", text: "hue-rules.node.disabled"});
}
else
{
scope.status({fill: "green", shape: "dot", text: "hue-rules.node.enabled"});
}
if(msg.payload === currentState.payload.enabled) { return false; }
patchObject["status"] = (msg.payload == true) ? 'enabled' : 'disabled';

// SEND MESSAGE
if(!config.skipevents)
{
var hueRule = new HueRulesMessage(rule, lastState);
send(hueRule.msg);
// PATCH!
bridge.patch("rules", "/rules/"+config.ruleid, patchObject, 1)
.then(function(response) { bridge.refetchRule(tempRuleID); })
.catch(function(errors) { scope.error(errors); });

// SAVE LAST STATE
lastState = rule;
}
if(done) { done(); }
})
.catch(error => {
scope.error(error, msg);
if(done) { done(error); }
});
// DONE?
if(done) { done(); }
}
});
else
{
// SET LAST COMMAND
if(scope.lastCommand !== null)
{
currentState.command = scope.lastCommand;
}

//
// CLOSE NODE / REMOVE EVENT LISTENER
this.on('close', function()
{
bridge.events.removeAllListeners('rule' + config.ruleid);
// SEND STATE
scope.send(currentState);

// RESET LAST COMMAND
scope.lastCommand = null;

if(done) {done();}
}
});
}

Expand Down
209 changes: 95 additions & 114 deletions huemagic/hue-scene.html
Original file line number Diff line number Diff line change
@@ -1,189 +1,170 @@
<script type="text/x-red" data-template-name="hue-scene">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="hue-scene.config.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-scene.config.input-name">
<input type="text" id="node-input-name" data-i18n="[placeholder]hue-scene.config.input-name" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-bridge"><i class="fa fa-server"></i> Bridge</label>
<input type="text" id="node-input-bridge">
<input type="text" id="node-input-bridge" style="width: calc(100% - 105px)">
</div>
<div class="form-row">
<label for="node-input-sceneid"><i class="fa fa-eye"></i> Scene</label>
<div style="display: inline-block; position: relative; width: 70%; height: 20px;">
<div style="position: absolute; left: 0px; right: 40px;">
<input type="text" id="node-input-sceneid" placeholder="1" style="width: 100%"/>
<label for="node-input-sceneid"><i class="fa fa-eye"></i> <span data-i18n="hue-scene.config.scene"></span></label>
<div style="display: inline-flex; width: calc(100% - 105px)">
<div id="input-scene-select-toggle" style="flex-grow: 1;">
<input type="text" id="node-input-sceneid" placeholder="00000000-0000-0000-0000-000000000000" style="width: 100%"/>
</div>
<a id="node-config-input-scan-scenes" class="editor-button" style="position: absolute; right: 0px; top: 0px;">
<button id="node-config-input-scan-scenes" type="button" class="red-ui-button" style="margin-left: 10px;">
<i class="fa fa-search"></i>
</a>
</button>
</div>
</div>
<div class="form-row">
<label for="node-input-groupid"><i class="fa fa-lightbulb-o"></i> <span data-i18n="hue-scene.config.group"></span></label>
<div style="display: inline-block; position: relative; width: 70%; height: 20px;">
<div style="position: absolute; left: 0px; right: 40px;">
<input type="text" id="node-input-groupid" placeholder="1" style="width: 100%"/>
</div>
<a id="node-config-input-scan-groups" class="editor-button" style="position: absolute; right: 0px; top: 0px;">
<i class="fa fa-search"></i>
</a>
</div>
</div>
<div class="form-row">
<label for="node-input-skipevents"><i class="fa fa-sign-out"></i> <span data-i18n="hue-scene.config.skipevents"></span></label>
<input type="checkbox" id="node-input-skipevents" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="hue-scene.config.skipevents-node"></span>
</label>
</div>
</script>

<script type="text/javascript">
RED.nodes.registerType('hue-scene',{
category: 'HueMagic',
color: '#ff823a',
defaults: {
name: {value:""},
bridge: {type: "hue-bridge", required: true},
sceneid: {value: null},
groupid: {value:"", required: false, validate: function(id) {
if(id.length < 1) { return true; }
else if(!isNaN(id)) { return true; }
else { return false; }
}},
skipevents: {value: false}
name: { value:"" },
bridge: { type: "hue-bridge", required: true },
sceneid: { value: null }
},
align: 'left',
icon: "hue-scene.png",
inputs: 1,
outputs: 1,
label: function() {
return this.name || this._("hue-scene.node.title");
},
paletteLabel: function() {
return this._("hue-scene.node.title");
},
inputLabels: function() {
return this._("hue-scene.node.input");
},
outputLabels: function() {
return this._("hue-scene.node.output");
},
align: 'right',
icon: "hue-scene.png",
paletteLabel: function() {
return this._("hue-scene.node.title");
},
label: function() {
return this.name || this._("hue-scene.node.title");
},
oneditprepare: function()
{
var scope = this;
const scope = this;
let sceneOptions = [];

function manualSceneID()
{
// GET CURRENT SELECTED VALUE
var current = $('#node-input-sceneid').val();
$('#node-input-sceneid').replaceWith('<input type="text" id="node-input-sceneid" style="width: 100%"/>');
$('#node-input-sceneid').val(current);
}

function manualGroupID()
{
var current = $('#node-input-groupid').val();
$('#node-input-groupid').replaceWith('<input type="text" id="node-input-groupid" style="width: 100%"/>');
$('#node-input-groupid').val(current);
// REMOVE SELECT FIELD
$('#input-scene-select-toggle').empty();

// CREATE NEW INPUT FIELD
$('#input-scene-select-toggle').append('<input type="text" id="node-input-sceneid" placeholder="00000000-0000-0000-0000-000000000000" style="width: 100%" value="'+current+'" />');

// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-scenes");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-pencil");
buttonIcon.addClass("fa-search");
}

function searchAndSelectScenes()
{
// GET CURRENT BRIDGE CONFIGURATION
var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
if(!bridgeConfig) { return false; }

// GET CURRENT SELECTED VALUE
var current = $('#node-input-sceneid').val();
$('#node-input-sceneid').replaceWith('<select id="node-input-sceneid" style="width: 100%"></select>');
$('#node-input-sceneid').append('<option selected="selected" value="null">'+scope._("hue-scene.config.searching")+'</option>');

var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
$.get('hue/scenes', {bridge: bridgeConfig.bridge, key: bridgeConfig.key})
.done(function(data) {
var scenes = JSON.parse(data);
// TRIGGER SEARCHING NOTIFICATION
var notification = RED.notify(scope._("hue-rules.config.searching"), { type: "compact", modal: true, fixed: true });

if(scenes.length <= 0)
// GET THE SCENES
$.get('hue/ressources', { bridge: bridgeConfig.bridge, key: bridgeConfig.key, type: "scene" })
.done( function(data) {
var allRessources = JSON.parse(data);
if(allRessources.length <= 0)
{
RED.notify(scope._("hue-scene.config.none-found"), "error");
notification.close();
RED.notify(scope._("hue-scene.config.none-found"), { type: "error" });
return false;
}

// RESET OPTIONS
$('#node-input-sceneid').empty();

// SET MOTION SENSORS AS OPTIONS
scenes.forEach(function(scene)
// SET OPTIONS
allRessources.forEach(function(ressource)
{
$('#node-input-sceneid').append('<option value="' + scene.id + '">' + scene.name + '</option>');
sceneOptions[ressource.id] = { value: ressource.id, label: ressource.name + " ("+ressource.group+")" };
});

// SELECT CURRENT VALUE
$('#node-input-sceneid').val(current);
})
.fail(function()
{
RED.notify(scope._("hue-scene.config.unknown-error"), "error");
});
}

function searchAndSelectGroupID()
{
var current = $('#node-input-groupid').val();
$('#node-input-groupid').replaceWith('<select id="node-input-groupid" style="width: 100%"></select>');
$('#node-input-groupid').append('<option selected="selected" value="null">'+scope._("hue-scene.config.group-searching")+'</option>');

var bridgeConfig = RED.nodes.node($('#node-input-bridge option:selected').val());
$.get('hue/groups', {bridge: bridgeConfig.bridge, key: bridgeConfig.key})
.done(function(data) {
var groups = JSON.parse(data);

if(groups.length <= 0)
{
RED.notify(scope._("hue-scene.config.group-none-found"), "error");
}

// RESET OPTIONS
$('#node-input-groupid').empty();

// SET GROUPS AS OPTIONS
$('#node-input-groupid').append('<option value="0">'+scope._("hue-scene.config.groups-all")+'</option>');
groups.forEach(function(group)
{
$('#node-input-groupid').append('<option value="' + group.id + '">' + group.name + '</option>');
$("#node-input-sceneid").typedInput({
types: [
{
value: current,
options: Object.values(sceneOptions)
}
]
});

// SELECT CURRENT VALUE
$('#node-input-groupid').val(current);
// CHANGE BUTTON ICON
var button = $("#node-config-input-scan-scenes");
var buttonIcon = button.find("i");
buttonIcon.removeClass("fa-search");
buttonIcon.addClass("fa-pencil");

// CLOSE SEARCH NOTIFICATION
notification.close();
})
.fail(function()
{
notification.close();
RED.notify(scope._("hue-scene.config.unknown-error"), "error");
});
}


$(document).on('change', '#node-input-sceneid', function()
// CHANGED SCENE ID? -> REPLACE NAME (IF POSSIBLE)
$(document).on('change', '#node-input-sceneid', function(e)
{
var sceneName = $('#node-input-sceneid option:selected').text();
if(sceneName.length > 0)
let currentSelectedOptionID = $(e.currentTarget).val();
let currentSelectedOptionValue = (currentSelectedOptionID.length > 0 && sceneOptions[currentSelectedOptionID]) ? sceneOptions[currentSelectedOptionID].label : false;

if(currentSelectedOptionValue !== false)
{
$('#node-input-name').val(sceneName);
$('#node-input-name').val(currentSelectedOptionValue.split(" (")[0]);
}
});

// SCAN
// TOGGLE SELECT/INPUT FIELD
$('#node-config-input-scan-scenes').click(function()
{
if($('#node-input-sceneid').prop("tagName") === "INPUT")
if($('#input-scene-select-toggle').find(".red-ui-typedInput-container").length > 0)
{
searchAndSelectScenes();
} else {
manualSceneID();
}
else
{
searchAndSelectScenes();
}
});

$('#node-config-input-scan-groups').click(function()
},
button: {
onclick: function()
{
if($('#node-input-groupid').prop("tagName") === "INPUT")
const node = this;
if(node.bridge)
{
searchAndSelectGroupID();
} else {
manualGroupID();
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify({ __user_inject_props__: "status"}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node.name + ": " + node._("hue-scene.node.statusmsg"), { type: "success", id: "status", timeout: 2000 });
}
});
}
});
}
}
});
</script>
159 changes: 64 additions & 95 deletions huemagic/hue-scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ module.exports = function(RED)
{
RED.nodes.createNode(this, config);

var scope = this;
let bridge = RED.nodes.getNode(config.bridge);
let { HueSceneMessage } = require('../utils/messages');
const scope = this;
const bridge = RED.nodes.getNode(config.bridge);

// GET TARGET WIRED GROUPS
this.targetGroups = {};
for (var w = scope.wires[0].length - 1; w >= 0; w--)
{
let oneWiredNode = RED.nodes.getNode(scope.wires[0][w]);
if(oneWiredNode && oneWiredNode.type == "hue-group" && oneWiredNode.exportedConfig && oneWiredNode.exportedConfig.groupid && oneWiredNode.exportedConfig.groupid.length > 1)
{
this.targetGroups[oneWiredNode.exportedConfig.groupid] = true;
}
}

//
// CHECK CONFIG
Expand All @@ -22,122 +32,81 @@ module.exports = function(RED)
// ENABLE SCENE
this.on('input', function(msg, send, done)
{
// Node-RED < 1.0
send = send || function() { scope.send.apply(scope,arguments); }
// REDEFINE SEND AND DONE IF NOT AVAILABLE
done = done || function() { scope.done.apply(scope,arguments); }

var groupID = config.groupid;
var sceneDef = config.sceneid;
// TARGET GROUP & SCENE
const groupIDS = (typeof msg.payload != 'undefined' && typeof msg.payload.group != 'undefined') ? msg.payload.group : [];
const sceneDef = (typeof msg.payload != 'undefined' && typeof msg.payload.scene != 'undefined') ? msg.payload.scene : config.sceneid;

// PASSED SCENE NAME?
if(typeof msg.payload === 'string' || msg.payload instanceof String)
{
sceneDef = msg.payload;
}
else
{
groupID = (typeof msg.payload != 'undefined' && typeof msg.payload.group != 'undefined') ? msg.payload.group : groupID;
sceneDef = (typeof msg.payload != 'undefined' && typeof msg.payload.scene != 'undefined') ? msg.payload.scene : sceneDef;
}
// PREPARE TARGET GROUPS
let copyOfTargetGroups = Object.keys(scope.targetGroups);
if(typeof groupIDS == 'string') { copyOfTargetGroups.push(groupIDS); }
else if(typeof groupIDS == 'object') { copyOfTargetGroups.concat(groupIDS); }
else { return false; }

if(config.sceneid)
{
bridge.client.scenes.getById(config.sceneid)
.then(scene => {
scope.proceedSceneAction(scene, groupID, send, done);
});
}
else if(sceneDef)
{
bridge.client.scenes.getAll()
.then(scenes => {
for (var scene of scenes)
{
if(scene.id == sceneDef)
{
scope.proceedSceneAction(scene, groupID, send, done);
break;
}
else if(scene.name == sceneDef)
{
scope.proceedSceneAction(scene, groupID, send, done);
break;
}
}
});
}
else
// CREATE PATCH
let patchObject = {};

// NO SCENE?
if(!sceneDef)
{
// ERROR
this.status({fill: "red", shape: "ring", text: "hue-scene.node.no-id"});
scope.error("Scene ID not found");
return false;
}
});


//
// PROCEED SCENE ACTION
this.proceedSceneAction = function(scene, applyOnGroup = false, send, done)
{
// CHECK IF SCENE SHOULD BE APPLIED TO A GROUP
if(applyOnGroup)
// RECALL ON GROUP? -> USE API v1
if(copyOfTargetGroups.length > 0)
{
var groupID = parseInt(applyOnGroup);
bridge.client.groups.getById(groupID)
.then(group =>
{
group.on = true;
group.scene = scene;
return bridge.client.groups.save(group);
})
.then(groupInfo =>
let targetSceneID = bridge.ressources[sceneDef].id_v1.replace("/scenes/", "");

// SEND STATUS
scope.status({fill: "blue", shape: "dot", text: "hue-scene.node.recalled-on-group"})

// FIND GROUP AND RECALL SCENE
for (var g = copyOfTargetGroups.length - 1; g >= 0; g--)
{
// SEND STATUS
scope.status({fill: "blue", shape: "dot", text: "hue-scene.node.recalled-on-group"});
let targetGroup = bridge.get("group", copyOfTargetGroups[g]);

// SEND MESSAGE
if(!config.skipevents)
if(targetGroup)
{
var hueScene = new HueSceneMessage(scene);
send(hueScene.msg);
bridge.patch("group", targetGroup.info.idV1 + "/action", { "scene": targetSceneID }, 1)
.catch(function(errors) {
scope.error(errors);
});
}
if(done) { done(); }
}

// RESET STATUS AFTER 3 SEC
setTimeout(function() {
scope.status({});
}, 3000);
})
.catch(error => {
scope.error(error, msg);
if(done) { done(error); }
});
// RESET STATUS AFTER 2 SECONDS
setTimeout(function() {
scope.status({});
}, 2000);
}
else
{
// RECALL A SCENE
bridge.client.scenes.recall(scene)
.then(recalledScene => {
// SEND STATUS
scope.status({fill: "blue", shape: "dot", text: "hue-scene.node.recalled"});
// RECALL SCENE
patchObject["recall"] = { status: "active" };

// SEND MESSAGE
if(!config.skipevents)
{
var hueScene = new HueSceneMessage(scene);
send(hueScene.msg);
}
// CHANGE NODE UI STATE
scope.status({fill: "grey", shape: "ring", text: "hue-scene.node.command"});

// PATCH!
bridge.patch("scene", sceneDef, patchObject)
.then(function()
{
scope.status({fill: "blue", shape: "dot", text: "hue-scene.node.recalled"});
if(done) { done(); }

// RESET STATUS AFTER 3 SEC
// RESET STATUS AFTER 1 SECOND
setTimeout(function() {
scope.status({});
}, 3000);
}, 1000);
})
.catch(error => {
scope.error(error, msg);
if(done) { done(error); }
});
.catch(function(errors) { scope.error(errors); });
}
}
});
}

RED.nodes.registerType("hue-scene", HueScene);
Expand Down
132 changes: 0 additions & 132 deletions huemagic/hue-switch.html

This file was deleted.

163 changes: 0 additions & 163 deletions huemagic/hue-switch.js

This file was deleted.

Loading