UNPKG

node-red-contrib-alexa-smart-home

Version:

Integrate Alexa and Google Assistant / Google Home in your Node-RED flows.

1,266 lines (1,197 loc) 47.5 kB
/** * Copyright 2016 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ module.exports = function (RED) { "use strict"; var mqtt = require("mqtt"); var bodyParser = require("body-parser"); var devices = {}; const https = require("https"); const tls = require("tls"); const semver = require("semver"); const { v4: uuidv4 } = require("uuid"); const packageJson = require("./package.json"); // TODO: Remove after NodeJS fix it, more information // https://github.com/nodejs/node/issues/16196 // https://github.com/nodejs/node/pull/16853 // This is fixed in Node 10, but this workaround also supports LTS versions // https://github.com/nodejs/node/pull/15206 if ( semver.gte(process.version, "8.6.0") && tls.DEFAULT_ECDH_CURVE === "prime256v1" ) { tls.DEFAULT_ECDH_CURVE = "auto"; } // Config Node function alexaConf(n) { RED.nodes.createNode(this, n); this.username = this.credentials.username || n.username; // enable transition to credential store for username this.password = this.credentials.password; this.mqttserver = n.mqttserver; this.webapiurl = n.webapiurl; this.contextName = n.contextName || "memory"; // enable transition to user-configurable context storage this.users = {}; var node = this; // console.log('***** received contextName: ' + n.contextName); // MQTT connect options var clientId = uuidv4(); // Generate UUID for use in clientId - clientId limit for Mosquitto is 65535 bytes var options = { username: node.username, password: node.password, //clientId: node.username, clientId: node.username + "-" + clientId, reconnectPeriod: 5000, servers: [ { protocol: "mqtts", host: node.mqttserver, port: 8883, }, { protocol: "mqtt", host: node.mqttserver, port: 1883, }, ], }; getDevices(node.webapiurl, node.username, node.password, node.id); this.connect = function () { // Log version to console to assist future debugging node.log("Node-RED contrib version: v" + packageJson.version); node.log( "Connecting to Alexa/ Google Home Skill MQTT server: " + node.mqttserver + ", account username: " + node.username ); node.client = mqtt.connect(options); node.client.setMaxListeners(0); node.client.on("connect", function () { node.log( "Successfully connected to Alexa/ Google Home Skill MQTT server: " + node.mqttserver + ", account username: " + node.username ); node.setStatus({ text: "connected", shape: "dot", fill: "green" }); node.client.removeAllListeners("message"); node.client.subscribe("command/" + node.username + "/#"); node.client.subscribe("message/" + node.username + "/#"); node.client.on("message", function (topic, message) { var msg = JSON.parse(message.toString()); //console.log("INFO, new MQTT message"); //console.log("INFO, message:" + JSON.stringify(msg)); // Message/ alert handler if (topic.indexOf("message") > -1) { var severity = msg.severity; var alert = msg.message; if (severity == "warn") { node.warn(alert); } else if (severity == "error") { node.error(alert); } } // Command handler if (topic.indexOf("command") > -1) { // Added Alexa message handler if (msg.hasOwnProperty("directive")) { //console.log("info", "Received Alexa MQTT message"); var endpointId = msg.directive.endpoint.endpointId; } // Google Home message handler if (msg.hasOwnProperty("execution")) { //console.log("info", "Received Google Home MQTT message"); var endpointId = msg.id; } for (var id in node.users) { if (node.users.hasOwnProperty(id)) { if ( node.users[id].device === endpointId && node.users[id].type == "alexa-smart-home-v3" ) { //console.log("info", "Sending command message"); node.users[id].command(msg); } } } } }); }); node.client.on("offline", function () { node.setStatus({ text: "disconnected", shape: "dot", fill: "red" }); }); node.client.on("reconnect", function () { node.warn( "Re-connecting to Alexa/ Google Home Skill MQTT server: " + node.mqttserver + ", account username: " + node.username ); node.setStatus({ text: "reconnecting", shape: "ring", fill: "red" }); }); node.client.on("error", function (err) { //console.log(err); node.setStatus({ text: "disconnected", shape: "dot", fill: "red" }); node.error(err); }); }; this.setStatus = function (status) { for (var id in node.users) { if (node.users.hasOwnProperty(id)) { node.users[id].status(status); } } }; this.register = function (deviceNode) { node.users[deviceNode.id] = deviceNode; // Object.keys(node.users).length will only === 1 at *first* node registration, thus connect will only execute once if (Object.keys(node.users).length === 1) { node.connect(); } }; this.deregister = function (deviceNode, done) { delete node.users[deviceNode.id]; if (Object.keys(node.users).length === 0) { //disconnect if (node.client && node.client.connected) { node.client.end(done); } else { node.client.end(); done(); } } done(); }; this.acknowledge = function (messageId, device, success) { var response = { messageId: messageId, success: success, }; //console.log("info, Response: " + JSON.stringify(response)); var topic = "response/" + node.username + "/" + device; if (node.client && node.client.connected) { node.client.publish(topic, JSON.stringify(response)); } }; // ########################################################## // Config Node Update State this.updateState = function (messageId, endpointId, payload, deviceName) { var response = { messageId: messageId, payload: { state: { brightness: payload.state.brightness, colorBrightness: payload.state.colorBrightness, colorHue: payload.state.colorHue, colorSaturation: payload.state.colorSaturation, colorTemperature: payload.state.colorTemperature, contact: payload.state.contact, input: payload.state.input, lock: payload.state.lock, motion: payload.state.motion, mute: payload.state.mute, percentage: payload.state.percentage, percentageDelta: payload.state.percentageDelta, playback: payload.state.playback, power: payload.state.power, rangeValue: payload.state.rangeValue, rangeValueDelta: payload.state.rangeValueDelta, temperature: payload.state.temperature, thermostatMode: payload.state.thermostatMode, thermostatSetPoint: payload.state.thermostatSetPoint, targetSetpointDelta: payload.state.targetSetpointDelta, volume: payload.state.volume, volumeDelta: payload.state.volumeDelta, }, }, }; var topic = "state/" + node.username + "/" + endpointId; node.log( deviceName + " : sending state update, topic:" + topic + " message:" + JSON.stringify(response) ); if (node.client && node.client.connected) { node.client.publish(topic, JSON.stringify(response)); } }; // ########################################################## this.on("close", function () { if (node.client && node.client.connected) { node.client.end(); } //node.removeAllListeners(); //delete devices[node.id]; }); } // Re-branded for v3 API RED.nodes.registerType("alexa-smart-home-v3-conf", alexaConf, { credentials: { username: { type: "text" }, password: { type: "password" }, }, }); // Command Node function alexaHome(n) { RED.nodes.createNode(this, n); this.conf = RED.nodes.getNode(n.conf); this.confId = n.conf; this.device = n.device; this.topic = n.topic; this.acknowledge = n.acknowledge; this.name = n.name; this.type = n.type; var node = this; // Command Node Command Function node.command = function (message) { //console.log("message", message) var messageFormat; // Alexa-format message handler if (message.hasOwnProperty("directive")) { //console.log("Alexa message", message) messageFormat = "Alexa"; var msg = { topic: node.topic || "", name: node.name, _messageId: message.directive.header.messageId, _endpointId: message.directive.endpoint.endpointId, _confId: node.confId, command: message.directive.header.name, extraInfo: message.directive.endpoint.cookie, }; } // Google-Home format message handler else if (message.hasOwnProperty("execution")) { //console.log("Google Home message", message) messageFormat = "Google Home"; var msg = { topic: node.topic || "", name: node.name, _messageId: message.requestId, _endpointId: message.execution.devices[0].id, _confId: node.confId, command: message.execution.execution[0].command, params: message.execution.execution[0].params, }; } var respond = true; var messageId; var supportedCommand = true; //console.log("Message: " + JSON.stringify(message)); // Alexa Message Handler if (messageFormat == "Alexa") { if (message.directive.header.hasOwnProperty("messageId")) { messageId = message.directive.header.messageId; } switch (message.directive.header.name) { case "Activate": // Scene Controller msg.payload = "ON"; break; case "AdjustBrightness": // Brightness % command msg.payload = message.directive.payload.brightnessDelta; break; case "AdjustPercentage": // Percentage Controller command msg.payload = message.directive.payload.percentageDelta; break; case "AdjustRangeValue": // Range Controller command msg.payload = message.directive.payload.rangeValueDelta; break; case "AdjustTargetTemperature": // Thermostat command msg.payload = message.directive.payload.targetSetpointDelta.value; msg.temperatureScale = message.directive.payload.targetSetpointDelta.scale; break; case "AdjustVolume": // Alexa.StepSpeaker if (message.directive.payload.hasOwnProperty("volumeSteps")) { msg.payload = message.directive.payload.volumeSteps; } // Alexa.Speaker else if (message.directive.payload.hasOwnProperty("volume")) { msg.payload = message.directive.payload.volume; } break; case "ChangeChannel": // Change channel command if ( typeof message.directive.payload.channel.number != "undefined" ) { msg.payload = message.directive.payload.channel.number; } else if ( message.directive.payload.channelMetadata.hasOwnProperty("name") ) { msg.payload = message.directive.payload.channelMetadata.name; } break; case "DecreaseColorTemperature": // Increase Color Temperature command msg.payload = "DecreaseColorTemperature"; break; case "FastForward": // FastForward command msg.payload = "FastForward"; break; case "IncreaseColorTemperature": // Increase Color Temperature command msg.payload = "IncreaseColorTemperature"; break; case "Lock": // Lock command msg.payload = "Lock"; break; case "Next": // Next command msg.payload = "Next"; break; case "Pause": // Pause command msg.payload = "Pause"; break; case "Play": // Play command msg.payload = "Play"; break; case "Previous": // Previous command msg.payload = "Previous"; break; case "Rewind": // Rewind command msg.payload = "Rewind"; break; case "SelectInput": // Select input command msg.payload = message.directive.payload.input; break; case "SetBrightness": // Brightness % command msg.payload = message.directive.payload.brightness; break; case "SetColor": // Color command msg.payload = message.directive.payload.color; break; case "SetColorTemperature": // Color command msg.payload = message.directive.payload.colorTemperatureInKelvin; break; case "SetMode": // SetMode command msg.payload = message.directive.payload.mode; break; case "SetMute": // Mute command if (message.directive.payload.mute == false) { msg.payload = "OFF"; } if (message.directive.payload.mute == true) { msg.payload = "ON"; } break; case "SetPercentage": // Percentage Controller command msg.payload = message.directive.payload.percentage; break; case "SetRangeValue": // Range Controller command msg.payload = message.directive.payload.rangeValue; break; case "SetTargetTemperature": // Thermostat command msg.payload = message.directive.payload.targetSetpoint.value; msg.temperatureScale = message.directive.payload.targetSetpoint.scale; break; case "SetThermostatMode": // Thermostat command msg.payload = message.directive.payload.thermostatMode.value; break; case "SetVolume": // Speaker command msg.payload = message.directive.payload.volume; break; case "StartOver": // StartOver command msg.payload = "StartOver"; break; case "Stop": // Stop command msg.payload = "Stop"; break; case "TurnOn": // Power-on command msg.payload = "ON"; break; case "TurnOff": // Power-off command msg.payload = "OFF"; break; case "Unlock": // Unlock command msg.payload = "Unlock"; break; default: // Do not handle unsupported commands node.warn("Alexa command unsupported!"); supportedCommand = false; break; } } // Google Home Message Handler else if (messageFormat == "Google Home") { if (message.hasOwnProperty("requestId")) { messageId = message.requestId; } switch (msg.command) { case "action.devices.commands.ActivateScene": msg.command = "Activate"; msg.payload = "ON"; break; case "action.devices.commands.BrightnessAbsolute": if (msg.params.hasOwnProperty("brightness")) { msg.command = "SetBrightness"; msg.payload = msg.params.brightness; } break; case "action.devices.commands.ColorAbsolute": if (msg.params.color.hasOwnProperty("temperature")) { msg.command = "SetColorTemperature"; msg.payload = msg.params.color.temperature; } if (msg.params.color.hasOwnProperty("spectrumHSV")) { msg.command = "SetColor"; msg.payload = { hue: msg.params.color.spectrumHSV.hue, saturation: msg.params.color.spectrumHSV.saturation, brightness: msg.params.color.spectrumHSV.value, }; } break; case "action.devices.commands.LockUnlock": if (msg.params.hasOwnProperty("lock")) { if (msg.params.lock == true) { msg.command = "Lock"; delete msg.payload; msg.payload = "Lock"; } else { msg.command = "Unlock"; delete msg.payload; msg.payload = "Unlock"; } } break; case "action.devices.commands.mediaPause": msg.command = "Pause"; delete msg.payload; break; case "action.devices.commands.mediaResume": msg.command = "Play"; delete msg.payload; break; case "action.devices.commands.mediaNext": msg.command = "Next"; delete msg.payload; break; case "action.devices.commands.mediaPrevious": msg.command = "Previous"; delete msg.payload; break; case "action.devices.commands.mediaStop": msg.command = "Stop"; delete msg.payload; break; case "action.devices.commands.mediaSeekRelative": if (msg.params.relativePositionMs < 0) { msg.command = "Rewind"; } if (msg.params.relativePositionMs > 0) { msg.command = "FastForward"; } delete msg.payload; break; case "action.devices.commands.mediaSeekToPosition": if ((msg.params.absPositionMs = 0)) { msg.command = "StartOver"; } delete msg.payload; break; case "action.devices.commands.OnOff": if (msg.params.on == true) { msg.command = "TurnOn"; msg.payload = "ON"; } else if (msg.params.on == false) { msg.command = "TurnOff"; msg.payload = "OFF"; } break; case "action.devices.commands.OpenClose": msg.command = "SetRangeValue"; msg.payload = msg.params.openPercent; break; case "action.devices.commands.SetFanSpeed": msg.command = "SetRangeValue"; msg.payload = msg.params.fanSpeed; break; case "action.devices.commands.setVolume": if (msg.params.hasOwnProperty("volumeLevel")) { msg.command = "SetVolume"; msg.payload = msg.params.volumeLevel; } break; case "action.devices.commands.ThermostatTemperatureSetpoint": if (msg.params.hasOwnProperty("thermostatTemperatureSetpoint")) { msg.command = "SetTargetTemperature"; msg.payload = msg.params.thermostatTemperatureSetpoint; } break; case "action.devices.commands.ThermostatSetMode": if (msg.params.hasOwnProperty("thermostatMode")) { msg.command = "SetThermostatMode"; msg.payload = msg.params.thermostatMode.toUpperCase(); } break; case "action.devices.commands.volumeRelative": if (msg.params.hasOwnProperty("volumeRelativeLevel")) { msg.command = "AdjustVolume"; msg.payload = msg.params.volumeRelativeLevel; } break; default: // Do not handle unsupported commands node.warn("Google Assistant command unsupported!"); supportedCommand = false; break; } } msg.acknowledge = {}; if (node.acknowledge) { msg.acknowledge = true; } else { msg.acknowledge = false; } if (supportedCommand == true) { node.send(msg); if (node.acknowledge && respond && messageId) { node.conf.acknowledge(messageId, node.device, true); } } }; if (node.conf) { node.conf.register(node); } else { node.warn("Unable to register device node, account not configured!"); } node.on("close", function (done) { node.conf.deregister(node, done); }); } // Re-branded for v3 API RED.nodes.registerType("alexa-smart-home-v3", alexaHome); // Think this is OK for v3 API function alexaHomeResponse(n) { RED.nodes.createNode(this, n); var node = this; node.on("input", function (msg) { if (msg._messageId && msg._endpointId && msg._confId) { var conf = RED.nodes.getNode(msg._confId); if (typeof msg.acknowledge == "boolean" && msg.acknowledge) { conf.acknowledge(msg._messageId, msg._endpointId, true); } else { conf.acknowledge(msg._messageId, msg._endpointId, false); } } }); } // ########################################################## // Set State Node function alexaHomeState(n) { RED.nodes.createNode(this, n); this.conf = RED.nodes.getNode(n.conf); this.confId = n.conf; this.device = n.device; this.name = n.name; this.type = n.type; var nodeContext = this.context(); var node = this; var onGoingCommands = {}; // console.log('***** config.contextName: ' + JSON.stringify(node.conf.contextName)); node.contextName = node.conf.contextName || "memory"; // set to 'memory' where config will be missing this on update for existing users // Timer to rate limit messages var timer = setInterval(function () { var now = Date.now(); var keys = Object.keys(onGoingCommands); var key; nodeContext.set("tmpCommand", "", node.contextName); nodeContext.set("tmpKey", "", node.contextName); for (key in keys) { var stateUpdate = onGoingCommands[keys[key]]; if (stateUpdate) { if ( !nodeContext.get("tmpCommand", node.contextName) || nodeContext.get("tmpCommand", node.contextName) == "" ) { // Capture first state update nodeContext.set( "tmpCommand", onGoingCommands[keys[key]], node.contextName ); nodeContext.set("tmpKey", key, node.contextName); } else { // If newer command same as previous, delete previous //console.log("debug, Timer GET stateUpdate keys:" + Object.keys(stateUpdate.payload.state)); //console.log("debug, Timer GET tmpCommand keys:" + Object.keys(nodeContext.get('tmpCommand',node.contextName).payload.state)); // if (Object.keys(stateUpdate.payload.state).toString() == Object.keys(nodeContext.get('tmpCommand',node.contextName).payload.state).toString() && stateUpdate.messageId != nodeContext.get('tmpCommand',node.contextName).messageId) { if ( Object.keys(stateUpdate.payload.state).toString() == Object.keys( nodeContext.get("tmpCommand", node.contextName).payload.state ).toString() ) { node.log( "Timer throttled/ deleted state update: " + keys[nodeContext.get("tmpKey", node.contextName)] ); delete onGoingCommands[ keys[nodeContext.get("tmpKey", node.contextName)] ]; nodeContext.set( "tmpCommand", onGoingCommands[keys[key]], node.contextName ); nodeContext.set("tmpKey", key, node.contextName); } else { //console.log("debug, Timer No match of object keys"); nodeContext.set( "tmpCommand", onGoingCommands[keys[key]], node.contextName ); nodeContext.set("tmpKey", key, node.contextName); } } var diff = now - stateUpdate.timestamp; if (diff > 1000) { node.conf.updateState( stateUpdate.messageId, stateUpdate.endpointId, stateUpdate.payload, node.name ); //console.log("debug, Timer sent state update: " + keys[key]); delete onGoingCommands[keys[key]]; } } } }, 250); // 250 Millisecond Timer // Store timer Id in node content nodeContext.set("timer", timer, node.contextName); // Set State Node On Input Function node.on("input", function (msg) { // State update could be for any state(s), validate the state message falls within expected params var stateValid = true; var statelessCommand = false; // Handle AlexaHome output if (msg.command == "AdjustPercentage") { msg.payload = { state: { percentageDelta: msg.payload } }; } else if (msg.command == "AdjustTargetTemperature") { msg.payload = { state: { targetSetpointDelta: msg.payload } }; } else if (msg.command == "AdjustVolume") { msg.payload = { state: { volumeDelta: msg.payload } }; } else if (msg.command == "AdjustRangeValue") { msg.payload = { state: { rangeValueDelta: msg.payload } }; } else if (msg.command == "Lock") { msg.payload = { state: { lock: "LOCKED" } }; } else if (msg.command == "SetBrightness") { msg.payload = { state: { brightness: msg.payload } }; } else if (msg.command == "SetColor") { msg.payload = { state: { colorHue: msg.payload.hue, colorSaturation: msg.payload.saturation, colorBrightness: msg.payload.brightness, }, }; } else if (msg.command == "SetColorTemperature") { msg.payload = { state: { colorTemperature: msg.payload } }; } else if (msg.command == "SelectInput") { msg.payload = { state: { input: msg.payload } }; } else if (msg.command == "SetMode") { msg.payload = { state: { mode: msg.payload } }; } else if (msg.command == "SetMute") { msg.payload = { state: { mute: msg.payload } }; } else if (msg.command == "SetPercentage") { msg.payload = { state: { percentage: msg.payload } }; } else if (msg.command == "SetRangeValue") { msg.payload = { state: { rangeValue: msg.payload } }; } else if (msg.command == "SetTargetTemperature") { msg.payload = { state: { thermostatSetPoint: msg.payload } }; } else if (msg.command == "SetThermostatMode") { msg.payload = { state: { thermostatMode: msg.payload } }; } else if (msg.command == "SetVolume") { msg.payload = { state: { volume: msg.payload } }; } else if (msg.command == "TurnOff" || msg.command == "TurnOn") { msg.payload = { state: { power: msg.payload } }; } else if (msg.command == "Unlock") { msg.payload = { state: { lock: "UNLOCKED" } }; } else { var arrayStatelessCommands = [ "Play", "Resume", "Pause", "FastFoward", "Rewind", "Previous", "Next", "StartOver", ]; if (arrayStatelessCommands.indexOf(msg.command) > -1) { node.log( node.name + " state node: 'stateless' command received, dropping message (expected for specific commands)." ); statelessCommand = true; } else if (msg.command) { node.warn( node.name + " state node: message object includes unexpected or invalid msg.command, please remove this from payload: " + msg.command ); } } // Add msg.acknowledge to message if not derived from an Alexa/ Google Home command if (msg.hasOwnProperty("command") == false) { msg.acknowledge = true; } // Adjusted to send state update after any Alexa/ Google Home command if ( msg.hasOwnProperty("command") == false && statelessCommand == false && nodeContext.get("lastPayload", node.contextName) && msg.payload.hasOwnProperty("state", node.contextName) ) { //console.log("debug, ON Message, lastpayload: " + JSON.stringify(nodeContext.get('lastPayload',node.contextName))); //console.log("debug, ON Message, msg.payload: " + JSON.stringify(msg.payload)); // Duplicate Payload to last payload received, discard unless an adjustment payload which is likely to be duplicate if ( JSON.stringify(nodeContext.get("lastPayload", node.contextName)) == JSON.stringify(msg.payload) && !( msg.payload.state.hasOwnProperty("percentageDelta") || msg.payload.state.hasOwnProperty("targetSetpointDelta") || msg.payload.state.hasOwnProperty("volumeDelta") ) ) { nodeContext.set("duplicatePayload", true, node.contextName); } // Non-duplicate payload. send to Web API else { nodeContext.set("duplicatePayload", false, node.contextName); nodeContext.set("lastPayload", msg.payload, node.contextName); } } else { nodeContext.set("duplicatePayload", false, node.contextName); nodeContext.set("lastPayload", msg.payload, node.contextName); } // Set State Payload Handler if ( statelessCommand == false && msg.hasOwnProperty("payload") && msg.payload.hasOwnProperty("state") && msg.hasOwnProperty("acknowledge") && nodeContext.get("duplicatePayload", node.contextName) == false ) { // Perform validation of device state payload, expects payload.state to contain as below // "brightness": payload.state.brightness, // "colorBrightness": payload.state.colorBrightness, // "colorHue": payload.state.colorHue, // "colorSaturation": payload.state.colorSaturation, // "colorTemperature": payload.state.colorTemperature, // "contact": payload.state.contact, // "input": payload.state.input, // "lock": payload.state.lock, // "mode": payload.state.mode, // "motion": payload.state.motion, // "mute": payload.state.mute, // "percentage": payload.state.percentage, // "percentageDelta": payload.state.percentageDelta, // "playback": payload.state.playback, // "power": payload.state.power, // "rangeValue": payload.state.rangeValue, // "rangeValueDelta": payload.state.rangeValueDelta, // "temperature": payload.state.temperature, // "targetSetpointDelta": payload.state.targetSetpointDelta, // "thermostatMode": payload.state.thermostatMode, // "thermostatSetPoint" : payload.state.thermostatSetPoint // "volume" : payload.state.thermostatSetPoint // "volumeDelta" : payload.state.thermostatSetPoint // Brightness state, expect state to be a number in range of 0-100 if (msg.payload.state.hasOwnProperty("brightness")) { if ( typeof msg.payload.state.brightness != "number" && (msg.payload.state.brightness < 0 || msg.payload.state.brightness > 100) ) { stateValid = false; } //node.log(node.name + " state node: brightness command received") //node.log(node.name + " " + JSON.stringify(msg.payload)); } // If *both* color and colorTemperature state sent, warn and do not send if ( msg.payload.state.hasOwnProperty("colorHue") && msg.payload.state.hasOwnProperty("colorSaturation") && msg.payload.state.hasOwnProperty("colorBrightness") && msg.payload.state.hasOwnProperty("colorTemperature") ) { node.warn( node.name + " state node: you cannot send combined 'colorTemperatrure' and 'color' state updates, send most recent update/ change only" ); stateValid = false; } // Color state, expect state to include hue, saturation and brightness, in range of 0-360 for hue and 0-1 for saturation and brightness else if ( msg.payload.state.hasOwnProperty("colorHue") && msg.payload.state.hasOwnProperty("colorSaturation") && msg.payload.state.hasOwnProperty("colorBrightness") ) { if ( typeof msg.payload.state.colorHue != "number" && typeof msg.payload.state.colorSaturation != "number" && typeof msg.payload.state.colorBrightness != "number" && msg.payload.state.colorHue < 0 && msg.payload.state.colorHue > 360 && msg.payload.state.colorSaturation < 0 && msg.payload.state.colorSaturation > 1 && msg.payload.state.colorBrightness < 0 && msg.payload.state.colorBrightness > 1 ) { stateValid = false; } } // Color Temperature, expect state to include colorTemperatureInKelvin, in range of 0-10000 else if (msg.payload.state.hasOwnProperty("colorTemperature")) { if ( typeof msg.payload.state.colorTemperature != "number" && (msg.payload.state.colorTemperature < 0 || msg.payload.state.colorTemperature > 10000) ) { stateValid = false; } } // Contact Sensor state, expect state to be a string if (msg.payload.state.hasOwnProperty("contact")) { if ( typeof msg.payload.state.contact != "string" && (msg.payload.state.contact != "DETECTED" || msg.payload.state.contact != "NOT_DETECTED") ) { stateValid = false; } } // Input state, expect string, inputs will grow so no point in specific string checking if (msg.payload.state.hasOwnProperty("input")) { if (typeof msg.payload.state.input != "string") { stateValid = false; } } // Lock state, expect string, either LOCKED or UNLOCKED if (msg.payload.state.hasOwnProperty("lock")) { if ( typeof msg.payload.state.lock != "string" && (msg.payload.state.lock != "LOCKED" || msg.payload.state.lock != "UNLOCKED") ) { stateValid = false; } } // Mode state, expect string, no point in specific string checking if (msg.payload.state.hasOwnProperty("mode")) { if (typeof msg.payload.state.mode != "string") { stateValid = false; } } // Motion Sensor state, expect state to be a string if (msg.payload.state.hasOwnProperty("motion")) { if ( typeof msg.payload.state.motion != "string" && (msg.payload.state.motion != "DETECTED" || msg.payload.state.motion != "NOT_DETECTED") ) { stateValid = false; } } // Mute state, expect string, either ON or OFF if (msg.payload.state.hasOwnProperty("mute")) { if ( typeof msg.payload.state.mute != "string" && (msg.payload.state.mute != "ON" || msg.payload.state.mute != "OFF") ) { stateValid = false; } else { if (msg.payload.state.mute == "ON") { msg.payload.state.mute = true; } else if (msg.payload.state.mute == "OFF") { msg.payload.state.mute = false; } } } // Percentage state, expect state top be number between 0 and 100 if (msg.payload.state.hasOwnProperty("percentage")) { if ( typeof msg.payload.state.percentage != "number" && (msg.payload.state.percentage < 0 || msg.payload.state.percentage > 100) ) { stateValid = false; } } // PercentageDelta state, expect state top be number between 0 and 100 if (msg.payload.state.hasOwnProperty("percentageDelta")) { if ( typeof msg.payload.state.percentageDelta != "number" && (msg.payload.state.percentageDelta < -100 || msg.payload.state.percentageDelta > 100) ) { stateValid = false; } } // Power state, expect state to be string, either ON or OFF if (msg.payload.state.hasOwnProperty("power")) { if ( typeof msg.payload.state.power != "string" && (msg.payload.state.power != "ON" || msg.payload.state.power != "OFF") ) { stateValid = false; } } // Range value, expect number if (msg.payload.state.hasOwnProperty("rangeValue")) { if (typeof msg.payload.state.rangeValue != "number") { stateValid = false; } } // Range value delta, expect number if (msg.payload.state.hasOwnProperty("rangeValueDelta")) { if (typeof msg.payload.state.rangeValueDelta != "number") { stateValid = false; } } // Temperature sensor state, expect state to be a number if (msg.payload.state.hasOwnProperty("temperature")) { if (typeof msg.payload.state.temperature != "number") { stateValid = false; } } // ThermostatMode state, expect state to be a number if (msg.payload.state.hasOwnProperty("thermostatMode")) { if (typeof msg.payload.state.thermostatMode != "string") { stateValid = false; } } // TargetSetpointDelta state, expect state to be a number if (msg.payload.state.hasOwnProperty("targetSetpointDelta")) { if (typeof msg.payload.state.targetSetpointDelta != "number") { stateValid = false; } } // ThermostatSetPoint state, expect state to be a number if (msg.payload.state.hasOwnProperty("thermostatSetPoint")) { if (typeof msg.payload.state.thermostatSetPoint != "number") { stateValid = false; } } // Volume state, expect state to be a number if (msg.payload.state.hasOwnProperty("volume")) { if (typeof msg.payload.state.volume != "number") { stateValid = false; } } // VolumeDelta state, expect state to be a number if (msg.payload.state.hasOwnProperty("volumeDelta")) { if (typeof msg.payload.state.volumeDelta != "number") { stateValid = false; } } if (stateValid && msg.acknowledge == true) { // Send messageId, deviceId, capability and payload to updateState var messageId = uuid(); //node.conf.updateState(messageId, this.device, msg.payload); var command = { messageId: messageId, endpointId: this.device, payload: msg.payload, timestamp: Date.now(), }; onGoingCommands[messageId] = command; } else if (stateValid && msg.acknowledge != true) { // Either auto-acknowledge is enabled on sender node, or validation has taken place node.warn( node.name + " state node: valid state update but msg.payload.acknowledge is false/ invalid/ missing" ); } else { // State update not valid, logic above will explain why node.warn( node.name + " state node: msg.payload.state not valid, check data types (numbers are not strings etc.) / format of state element" ); } } // Payload missing else if (statelessCommand == false && !msg.hasOwnProperty("payload")) { node.warn(node.name + " state node: msg.payload missing!"); } // State missing else if ( statelessCommand == false && !msg.payload.hasOwnProperty("state") ) { node.warn( node.name + " state node: msg.payload missing state element!" ); } // Acknowledge missing else if ( statelessCommand == false && !msg.hasOwnProperty("acknowledge") ) { node.warn(node.name + " state node: message missing msg.acknowledge"); } // Duplicate State Update else if (nodeContext.get("duplicatePayload", node.contextName) == true) { // node.log(node.name + " state node: discarded duplicate state payload"); } }); if (node.conf) { node.conf.register(node); } else { node.warn("Unable to register state node, account not configured!"); } node.on("close", function (done) { node.conf.deregister(node, done); clearInterval(nodeContext.get("timer", node.contextName)); // Close Interval Timer used node context stored Id }); } // ########################################################## // Re-branded for v3 API RED.nodes.registerType("alexa-smart-home-v3-resp", alexaHomeResponse); // New Node Type for State Reporting to Web App RED.nodes.registerType("alexa-smart-home-v3-state", alexaHomeState); // Re-branded for v3 API RED.httpAdmin.use("/alexa-smart-home-v3/new-account", bodyParser.json()); // Shouldn't need a change? // ## Changed to include url in expected params function getDevices(url, username, password, id) { if (url && username && password) { const options = { //ecdhCurve: 'auto', hostname: url, port: 443, path: "/api/v1/devices", method: "GET", auth: username + ":" + password, }; const req = https.request(options, (res) => { //console.log('statusCode:', res.statusCode); //console.log('headers:', res.headers); var body = ""; res.on("data", (d) => { if (res.statusCode == 200) { body = body + d; } // else { // console.log("Error: getDevices status code: " + res.statusCode); // console.log("Error: getDevices returned data: " + res.d); // } }); res.on("end", (d) => { if (res.statusCode == 200) { var devs = JSON.parse(body); devices[id] = devs; } else { console.log("Error: getDevices status code: " + res.statusCode); console.log("Error: getDevices returned data: " + res.d); } }); }); req.on("error", (e) => { console.log( "Error: getDevices unable to lookup devices for username: " + username ); console.log("Error: getDevices returned: " + e); }); req.end(); } } // UUID Generator function uuid() { var uuid = "", i, random; for (i = 0; i < 32; i++) { random = (Math.random() * 16) | 0; if (i == 8 || i == 12 || i == 16 || i == 20) { uuid += "-"; } uuid += (i == 12 ? 4 : i == 16 ? (random & 3) | 8 : random).toString(16); } return uuid; } // Re-branded for v3 API RED.httpAdmin.post("/alexa-smart-home-v3/new-account", function (req, res) { //console.log("httpAdmin post", req.body); var username = req.body.user; var password = req.body.pass; var url = req.body.webapi; var id = req.body.id; // ## Modified getDevices(url, username, password, id); }); // Re-branded for v3 API RED.httpAdmin.post("/alexa-smart-home-v3/refresh/:id", function (req, res) { var id = req.params.id; var conf = RED.nodes.getNode(id); if (conf) { var username = conf.credentials.username; var password = conf.credentials.password; var url = conf.webapiurl; getDevices(url, username, password, id); res.status(200).send(); } else { //not deployed yet RED.warn("Can't refresh devices until deployed"); res.status(404).send(); } }); // Re-branded for v3 API RED.httpAdmin.get("/alexa-smart-home-v3/devices/:id", function (req, res) { if (devices[req.params.id]) { res.send(devices[req.params.id]); } else { res.status(404).send(); } }); };