UNPKG

node-red-contrib-huemagic

Version:

Philips Hue node to control bridges, lights, groups, scenes, rules, taps, switches, buttons, motion sensors, temperature sensors and Lux sensors using Node-RED.

636 lines (572 loc) 17.7 kB
module.exports = function(RED) { "use strict"; function HueGroup(config) { RED.nodes.createNode(this, config); const scope = this; const bridge = RED.nodes.getNode(config.bridge); const async = require('async'); // EXPORT CONFIG this.exportedConfig = config; // SAVE FUTURE PATCH this.futurePatchState = {}; // SAVE LAST COMMAND this.lastCommand = null; // HELPER const colorUtils = require('./utils/color'); const merge = require('./utils/merge'); // // CHECK CONFIG if(bridge == null) { this.status({fill: "red", shape: "ring", text: "hue-group.node.not-configured"}); return false; } // // UNIVERSAL MODE? if(!config.groupid) { this.status({fill: "grey", shape: "dot", text: "hue-group.node.universal"}); } // // UPDATE STATE if(typeof bridge.disableupdates != 'undefined'||bridge.disableupdates == false) { this.status({fill: "grey", shape: "dot", text: "hue-group.node.init"}); } // // SUBSCRIBE TO UPDATES FROM THE BRIDGE bridge.subscribe("group", config.groupid, function(info) { let currentState = bridge.get("group", info.id, { colornames: config.colornamer ? true : false }); // RESOURCE FOUND? if(currentState !== false) { // NOT IN UNIVERAL MODE? -> CHANGE UI STATES if(config.groupid) { // APPLY FUTURE STATE COMMANDS if(Object.values(scope.futurePatchState).length > 0) { scope.applyCommands({}, null, null); } if(currentState.payload.on === true) { scope.status({fill: "yellow", shape: "dot", text: "hue-group.node.turned-on"}); } else { scope.status({fill: "grey", shape: "dot", text: "hue-group.node.all-off"}); } } // 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); // RESET LAST COMMAND scope.lastCommand = null; } } }); // // CONTROL GROUP this.on('input', function(msg, send, done) { scope.applyCommands(msg, send, done); }); // // APPLY COMMANDS (API v1 because CLIP/v2 does not yet support all features) this.applyCommands = async function(msg, send = null, done = null) { // SET SEND send = send || function() { scope.send.apply(scope,arguments); } // SAVE LAST COMMAND scope.lastCommand = RED.util.cloneMessage(msg); // CREATE PATCH let patchObject = {}; // DEFINE SENSOR ID & CURRENT STATE const tempGroupID = (!config.groupid && typeof msg.topic != 'undefined' && bridge.validResourceID.test(msg.topic) === true) ? msg.topic : config.groupid; let currentState = bridge.get("group", tempGroupID, { colornames: config.colornamer ? true : false }); if(!currentState) { scope.error("The group in not yet available. Please wait until HueMagic has established a connection with the bridge or check whether the resource ID in the configuration is valid."); return false; } // CHECK IF LIGHT ID IS SET if(!tempGroupID) { scope.error(RED._("hue-group.node.error-no-id")); return false; } // GET FUTURE STATE if(Object.values(scope.futurePatchState).length > 0) { patchObject = Object.assign({}, scope.futurePatchState); scope.futurePatchState = {}; } // GET CURRENT STATE if( (typeof msg.payload != 'undefined' && typeof msg.payload.status != 'undefined') || (typeof msg.__user_inject_props__ != 'undefined' && msg.__user_inject_props__ == "status") ) { // SET LAST COMMAND if(scope.lastCommand !== null) { currentState.command = scope.lastCommand; } // SEND STATE scope.send(currentState); // RESET LAST COMMAND scope.lastCommand = null; if(done) { done(); } return true; } // COLORLOOP EFFECT if(typeof msg.payload != 'undefined' && typeof msg.payload.colorloop != 'undefined' && msg.payload.colorloop > 0) { patchObject = { "on": true, "effect": "colorloop", "bri": msg.payload.brightness ? Math.round((254/100)*msg.payload.brightness) : 254 }; // PATCH! async.retry({ times: 3, errorFilter: function(err) { return (err.status == 503); }, interval: function(retryCount) { return retryCount*2000; } }, function(callback, results) { bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1) .then(function(status) { // RESET COLORLOOP ANIMATION AFTER X SECONDS setTimeout(function() { bridge.patch("group", currentState.info.idV1 + "/action", { "effect": "none" }, 1) .then(function() { if(done) { done(); }}); }, parseInt(msg.payload.colorloop) * 1000); callback(null, true); }) .catch(function(errors) { callback(errors, null); }); }, function(errors, success) { if(errors) { scope.error(errors); scope.status({fill: "red", shape: "ring", text: "hue-group.node.error-input"}); } else if(done) { done(); } }); return false; } // ALERT EFFECT if(typeof msg.payload != 'undefined' && typeof msg.payload.alert != 'undefined' && msg.payload.alert > 0) { // SAVE PREVIOUS STATE scope.context().set('groupPreviousState', currentState); // TURN ON LIGHT if(currentState.payload.on === false) { patchObject["on"] = true; } // SET BRIGHTNESS if(!msg.payload.brightness && currentState.payload.brightness != 100) { patchObject["bri"] = 254; } else if(msg.payload.brightness) { patchObject["bri"] = Math.round((254/100)*msg.payload.brightness); } // SET TRANSITION patchObject["transitiontime"] = 0; // CAN CHANGE COLOR? let XYAlertColor = {}; if(typeof msg.payload.rgb != 'undefined') { XYAlertColor = colorUtils.rgbToXy(msg.payload.rgb[0], msg.payload.rgb[1], msg.payload.rgb[2] ); } else if(typeof msg.payload.hex != 'undefined') { let rgbFromHex = colorUtils.hexRgb((msg.payload.hex).toString()); XYAlertColor = colorUtils.rgbToXy(rgbFromHex[0], rgbFromHex[1], rgbFromHex[2] ); } else if(typeof msg.payload.color != 'undefined') { if(new RegExp("random|any|whatever").test(msg.payload.color)) { const randomColor = colorUtils.randomHexColor(); let rgbFromHex = colorUtils.hexRgb(rgbFromHex); XYAlertColor = colorUtils.rgbToXy(rgbFromHex[0], rgbFromHex[1], rgbFromHex[2] ); } else { var colorHex = colorUtils.colornames(msg.payload.color); if(colorHex) { let rgbFromHex = colorUtils.hexRgb(colorHex); XYAlertColor = colorUtils.rgbToXy(rgbFromHex[0], rgbFromHex[1], rgbFromHex[2] ); } } } else { XYAlertColor = colorUtils.rgbToXy(255, 0, 0 ); } patchObject["xy"] = [XYAlertColor.x, XYAlertColor.y]; // SET ALERT EFFECT patchObject["alert"] = "lselect"; // PATCH! async.retry({ times: 3, errorFilter: function(err) { return (err.status == 503); }, interval: function(retryCount) { return retryCount*2000; } }, function(callback, results) { // 1. TURN ON THE LIGHT BULB bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1) .then(function(status) { setTimeout(function() { const tempPreviousState = scope.context().get('groupPreviousState'); var tempPreviousStatePatch = {}; tempPreviousStatePatch.dimming = { brightness: tempPreviousState.payload.brightness }; if(tempPreviousState.payload.xyColor) { tempPreviousStatePatch.xy = [tempPreviousState.payload.xyColor.x, tempPreviousState.payload.xyColor.y]; } else if(tempPreviousState.payload.colorTemp) { tempPreviousStatePatch.ct = tempPreviousState.payload.colorTemp; } bridge.patch("group", currentState.info.idV1 + "/action", tempPreviousStatePatch, 1) .then(function(status) { return bridge.patch("group", currentState.info.idV1 + "/action", { on: false }, 1); }) .then(function(status) { if(tempPreviousState.payload.on === true) { bridge.patch("group", currentState.info.idV1, { on: true }, 1); } }); }, parseInt(msg.payload.alert) * 1000); callback(null, true); }) .catch(function(errors) { callback(errors, null); }); }, function(errors, success) { if(errors) { scope.error(errors); scope.status({fill: "red", shape: "ring", text: "hue-group.node.error-input"}); } else if(done) { done(); } }); } // ANIMATION STARTED? else if(typeof msg.animation != 'undefined' && msg.animation.status == true && msg.animation.restore == true) { // SAVE PREVIOUS STATE scope.context().set('groupPreviousState', currentState); } // ANIMATION STOPPED AND RESTORE ACTIVE? else if(typeof msg.animation != 'undefined' && msg.animation.status == false && msg.animation.restore == true) { const tempPreviousState = scope.context().get('groupPreviousState'); var tempPreviousStatePatch = {}; tempPreviousStatePatch.dimming = { brightness: tempPreviousState.payload.brightness }; if(tempPreviousState.payload.xyColor) { tempPreviousStatePatch.xy = [tempPreviousState.payload.xyColor.x, tempPreviousState.payload.xyColor.y]; } else if(tempPreviousState.payload.colorTemp) { tempPreviousStatePatch.ct = tempPreviousState.payload.colorTemp; } // PATCH! async.retry({ times: 3, errorFilter: function(err) { return (err.status == 503); }, interval: function(retryCount) { return retryCount*2000; } }, function(callback, results) { bridge.patch("light", currentState.info.lightIds[l], tempPreviousStatePatch). then(function(status) { if(tempPreviousState.payload.on === false) { bridge.patch("light", currentState.info.lightIds[l], { on: { on: false } }) .then(function() { callback(null, true); }); } else { bridge.patch("light", currentState.info.lightIds[l], { on: { on: false } }) .then(function(status) { callback(null, true); return bridge.patch("light", currentState.info.lightIds[l], { on: { on: true } }); }); } }) .catch(function(errors) { callback(errors, null); }); }, function(errors, success) { if(errors) { scope.error(errors); } else if(done) { done(); } }); } // EXTENDED COMMANDS else { // SET LIGHT STATE SIMPLE MODE if(msg.payload === true||msg.payload === false) { if(msg.payload !== currentState.payload.on) { patchObject["on"] = msg.payload; } } // SET LIGHT STATE if(typeof msg.payload != 'undefined' && typeof msg.payload.on != 'undefined' && (msg.payload.on === true || msg.payload.on === false)) { if(msg.payload.on !== currentState.payload.on) { patchObject["on"] = msg.payload.on; } } // TOGGLE ON / OFF if(typeof msg.payload != 'undefined' && typeof msg.payload.toggle != 'undefined') { patchObject["on"] = !currentState.payload.on; } // SET BRIGHTNESS if(typeof msg.payload != 'undefined' && typeof msg.payload.brightness != 'undefined') { // AUTO BRIGHTNESS BASED ON DAY TIME if(new RegExp("auto|automatic").test(msg.payload.brightness)) { let ct = colorUtils.colorTemperature(); let autoBrightness = ((300-ct)/2)+100; autoBrightness = (autoBrightness > 100) ? 100 : autoBrightness; autoBrightness = (autoBrightness < 20) ? 20 : autoBrightness; // SET CALCULATED BRIGHTNESS patchObject["bri"] = Math.round((254/100)*autoBrightness); } else { if(msg.payload.brightness > 100 || msg.payload.brightness < 0) { scope.error("Invalid brightness setting. Only 0 - 100 percent allowed"); return false; } else if(msg.payload.brightness == 0) { patchObject["on"] = false; } else { patchObject["bri"] = Math.round((254/100)*msg.payload.brightness); } } } else if(typeof msg.payload != 'undefined' && typeof msg.payload.brightnessLevel != 'undefined') { if(msg.payload.brightnessLevel > 254 || msg.payload.brightnessLevel < 0) { scope.error("Invalid brightness setting. Only 0 - 254 allowed"); return false; } else if(msg.payload.brightness == 0) { patchObject["on"] = false; } else { patchObject["bri"] = msg.payload.brightnessLevel; } } // SET HUMAN READABLE COLOR OR RANDOM if(typeof msg.payload != 'undefined' && typeof msg.payload.color != 'undefined') { let XYAlertColor = {}; if(new RegExp("random|any|whatever").test(msg.payload.color)) { const randomColor = colorUtils.randomHexColor(); let rgbFromHex = colorUtils.hexRgb(randomColor); XYAlertColor = colorUtils.rgbToXy(rgbFromHex[0], rgbFromHex[1], rgbFromHex[2] ); } else { var colorHex = colorUtils.colornames(msg.payload.color); if(colorHex) { let rgbFromHex = colorUtils.hexRgb(colorHex); XYAlertColor = colorUtils.rgbToXy(rgbFromHex[0], rgbFromHex[1], rgbFromHex[2] ); } } patchObject["xy"] = [XYAlertColor.x, XYAlertColor.y]; } // SET HEX COLOR if(typeof msg.payload != 'undefined' && typeof msg.payload.hex != 'undefined') { let rgbFromHex = colorUtils.hexRgb((msg.payload.hex).toString()); let xyColor = colorUtils.rgbToXy(rgbFromHex[0], rgbFromHex[1], rgbFromHex[2]) patchObject["xy"] = [xyColor.x, xyColor.y]; } // SET RGB COLOR if(typeof msg.payload != 'undefined' && typeof msg.payload.rgb != 'undefined' && msg.payload.rgb.length === 3) { let xyColor = colorUtils.rgbToXy(msg.payload.rgb[0], msg.payload.rgb[1], msg.payload.rgb[2] ) patchObject["xy"] = [xyColor.x, xyColor.y]; } // SET XY COLOR if(typeof msg.payload != 'undefined' && typeof msg.payload.xyColor != 'undefined') { patchObject["xy"] = [msg.payload.xyColor.x, msg.payload.xyColor.y]; } // SET COLOR TEMPERATURE if(typeof msg.payload != 'undefined' && typeof msg.payload.colorTemp != 'undefined') { // DETERMINE IF AUTOMATIC, WARM, COLD, INT if(!isNaN(msg.payload.colorTemp)) { let colorTemp = parseInt(msg.payload.colorTemp); if(colorTemp >= 153 && colorTemp <= 500) { patchObject["ct"] = colorTemp; } else { scope.error("Invalid color temprature. Only 153 - 500 allowed"); return false; } } else if(msg.payload.colorTemp == "cold") { patchObject["ct"] = 153; } else if(msg.payload.colorTemp == "normal") { patchObject["ct"] = 240; } else if(msg.payload.colorTemp == "warm") { patchObject["ct"] = 400; } else if(msg.payload.colorTemp == "hot") { patchObject["ct"] = 500; } else { // SET TEMPERATURE patchObject["ct"] = colorUtils.colorTemperature(); } } // SET TRANSITION TIME if(typeof msg.payload != 'undefined' && typeof msg.payload.transitionTime != 'undefined') { let targetTransitionTime = parseFloat(msg.payload.transitionTime)*1000; targetTransitionTime = (targetTransitionTime > 6000000) ? 6000000 : targetTransitionTime; targetTransitionTime = (targetTransitionTime < 0) ? 0 : targetTransitionTime; patchObject["transitiontime"] = targetTransitionTime/100; } // SET DOMINANT COLORS FROM IMAGE if(typeof msg.payload != 'undefined' && typeof msg.payload.image != 'undefined') { var colors = await colorUtils.getColors(msg.payload.image); if(colors.length > 0) { var colorsHEX = colors.map(color => color.hex()); let rgbFromHex = colorUtils.hexRgb(colorsHEX[0]); let xyColor = colorUtils.rgbToXy(rgbFromHex[0], rgbFromHex[1], rgbFromHex[2]); patchObject["xy"] = [xyColor.x, xyColor.y]; } } // // SHOULD PATCH? if(Object.values(patchObject).length > 0) { // IS FOR LATER? if(currentState.payload.on === false) { if(!patchObject["on"]) { scope.futurePatchState = merge.deep(scope.futurePatchState, patchObject); return false; } } // PATCH! async.retry({ times: 3, errorFilter: function(err) { return (err.status == 503); }, interval: function(retryCount) { return retryCount*2000; } }, function(callback, results) { bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1) .then(function() { callback(null, true); }) .catch(function(errors) { callback(errors, null); }); }, function(errors, success) { if(errors) { scope.error(errors); } else if(done) { done(); } }); } else { // SET LAST COMMAND if(scope.lastCommand !== null) { currentState.command = scope.lastCommand; } // SEND STATE scope.send(currentState); // RESET LAST COMMAND scope.lastCommand = null; if(done) { done(); } } } } } RED.nodes.registerType("hue-group", HueGroup); }