node-red-contrib-smartthings
Version:
Querying/Controlling Smartthings Devices
361 lines (312 loc) • 13.2 kB
JavaScript
var Promise = require('promise');
module.exports = function(RED) {
/**
hsl/rgb functions reference:
https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
*/
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 100] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
*/
function hslToRgb(h, s, l) {
h/=100;s/=100;l/=100;
let r, g, b;
if(s == 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r*255), Math.round(g*255), Math.round(b*255)];
}
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 100].
*
* @param {number} r The red color value
* @param {number} g The green color value
* @param {number} b The blue color value
* @return {Array} The HSL representation
*/
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if(max == min){
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [Math.round(h*100), Math.round(s*100), Math.round(l*100)];
}
function SmartthingsColorNode(config) {
RED.nodes.createNode(this, config);
let node = this;
console.debug("SmartthingsColorNode")
console.debug(config);
this.conf = RED.nodes.getNode(config.conf);
this.name = config.name;
this.device = config.device;
this.state = {
value: 0,
level: 0,
levelUnit: "",
color: [0, 0, 0],
hue: 0,
saturation: 0,
temperature: 0,
temperatureUnit: "",
unsingColor: true
};
this.reportState = function(send, done, original) {
send = send || function() { node.send.apply(node,arguments) };
done = done || function() { };
let msg = [{
topic: "device",
payload: {
deviceId: this.device,
deviceType: "switch",
name: this.name,
value: this.state.value,
timestamp: new Date().toISOString()
}
},{
topic: "device",
payload: {
deviceId: this.device,
deviceType: "level",
name: this.name,
value: this.state.level,
unit: this.state.levelUnit,
timestamp: new Date().toISOString()
}
},{
topic: "device",
payload: {
deviceId: this.device,
deviceType: "color",
name: this.name,
value: this.state.color,
timestamp: new Date().toISOString()
}
},{
topic: "device",
payload: {
deviceId: this.device,
deviceType: "temperature",
name: this.name,
value: this.state.temperature,
unit: this.state.temperatureUnit,
timestamp: new Date().toISOString()
}
}];
if(original !== undefined){
msg.forEach( (m) => {
original.payload = m.payload;
Object.assign(m,original);
});
}
send(msg);
done();
};
this.setState = function(value, send, done){
Object.assign(this.state, value);
this.state.color = hslToRgb(this.state.hue, this.state.saturation, this.state.level);
this.reportState(send ,done);
};
this.pullState = function(){
this.conf.getDeviceStatus(this.device,"main").then( (status) => {
console.debug("ColorDevice("+this.name+") Status Refreshed");
let state = {};
if(status["switch"] !== undefined && status["switch"]["switch"] !== undefined){
state.value = (status["switch"]["switch"]["value"].toLowerCase() === "on" ? 1 : 0);
}
if(status["switchLevel"] !== undefined && status["switchLevel"]["level"] !== undefined){
state.level = status["switchLevel"]["level"]["value"];
state.levelUnit = status["switchLevel"]["level"]["unit"];
}
if(status["colorControl"] !== undefined){
state.hue = status["colorControl"]["hue"]["value"];
state.saturation = status["colorControl"]["saturation"]["value"];
}
if(status["colorTemperature"] !== undefined){
state.temperature = status["colorTemperature"]["colorTemperature"]["value"];
state.temperatureUnit = status["colorTemperature"]["colorTemperature"]["unit"];
}
this.setState(state);
}).catch( err => {
console.error("Ops... error getting device state (ColorDevice)");
console.error(err);
});
};
if(this.conf && this.device){
const callback = (evt) => {
console.debug("ColorDevice("+this.name+") Callback called");
console.debug(evt);
let state = {};
switch(evt["attribute"].toLowerCase()){
case "switch":
state.value = (evt["value"].toLowerCase() === "on" ? 1 : 0);
break;
case "level":
state.level = evt["value"];
break;
case "saturation":
state.saturation = evt["value"];
state.unsingColor = true;
break;
case "hue":
state.hue = evt["value"];
state.unsingColor = true;
break;
case "temperature":
state.temperature = evt["value"];
state.unsingColor = false;
break;
}
this.setState(state);
}
this.conf.registerCallback(this, this.device, callback);
this.pullState();
this.on('input', (msg, send, done) => {
send = send || function() { node.send.apply(node,arguments) };
done = done || function() { };
console.debug("Input Message Received");
console.log(msg);
if(msg && msg.topic !== undefined){
switch(msg.topic){
case "pull":
this.pullState();
break;
case "update":
this.reportState(send, done, msg);
break;
case "switch":
this.conf.executeDeviceCommand(this.device,[{
component: "main",
capability: "switch",
command: (msg.payload.value == 1 ? "on" : "off")
}]).then( (ret) => {
const state = {
value: msg.payload.value
}
this.setState(state, send, done);
}).catch( (ret) => {
console.error("Error updating device");
done("Error updating device");
});
break;
case "level":
this.conf.executeDeviceCommand(this.device,[{
component: "main",
capability: "switchLevel",
command: "setLevel",
arguments: [
msg.payload.value
]
}]).then( (ret) => {
const state = {
level: msg.payload.value
}
this.setState(state, send, done);
}).catch( (ret) => {
console.error("Error updating device");
done("Error updating device");
});
break;
case "color":
if(Array.isArray(msg.payload.value)){
const hsl = rgbToHsl(msg.payload.value[0], msg.payload.value[1], msg.payload.value[2]);
this.conf.executeDeviceCommand(this.device,[{
component: "main",
capability: "colorControl",
command: "setHue",
arguments: [
hsl[0]
]
},{
component: "main",
capability: "colorControl",
command: "setSaturation",
arguments: [
hsl[1]
]
},{
component: "main",
capability: "switchLevel",
command: "setLevel",
arguments: [
hsl[2]
]
}]).then( (ret) => {
const state = {
hue: hsl[0],
saturation: hsl[1],
level: hsl[2]
}
this.setState(state, send, done);
}).catch( (ret) => {
console.error("Error updating device");
done("Error updating device");
});
}
break;
case "temperature":
this.conf.executeDeviceCommand(this.device,[{
component: "main",
capability: "colorTemperature",
command: "setColorTemperature",
arguments: [
msg.payload.value
]
}]).then( (ret) => {
const state = {
temperature: msg.payload.value
}
this.setState(state, send, done);
}).catch( (ret) => {
console.error("Error updating device");
done("Error updating device");
});
break;
default:
done("Invalid Topic");
break;
}
}
});
this.on('close', () => {
console.debug("Closed");
this.conf.unregisterCallback(this, this.device, callback);
});
}
}
RED.nodes.registerType("smartthings-node-color", SmartthingsColorNode);
};