├── .gitignore ├── .travis.yml ├── README.md ├── config.js ├── modules ├── helper.js ├── parameter-extractor.js ├── plugin-manager.js ├── response-generator.js └── xml-rpc-api-handler.js ├── package.json ├── plugins ├── limitless-onleave-autooff.js ├── limitless-zone-onoff.js ├── sample-plugin.js ├── wakeonlan-linux.js └── win-shutdown-linux.js ├── server.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.DS_Store 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | 15 | node_modules 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IFTTN - If This Then Node 2 | [![Build Status](https://travis-ci.org/sebauer/if-this-then-node.svg?branch=master)](https://travis-ci.org/sebauer/if-this-then-node) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard) 3 | 4 | IFTTN is a NodeJS based server which allows you to receive actions from [IFTTT](http://www.ifttt.com). It can be used to run on a Raspberry PI in your local network to use IFTTT for further home automation and other tasks. 5 | 6 | The new version is based on [IFTTT "Maker"](http://ifttt.com/maker) feature and receives JSON requests to handle further actions. 7 | 8 | # Setup and Configuration 9 | 10 | git clone IFTTN to wherever you want and run [NPM](https://www.npmjs.org/) afterwards to install all dependencies: 11 | ``` 12 | git clone git@github.com:sebauer/if-this-then-node.git 13 | npm install 14 | ``` 15 | 16 | The next step is to set your custom authentication details in `config.js`. You need to use the credentials in the IFTTT WordPress channel. 17 | ```javascript 18 | var config = { 19 | 'user': 'myuser', // Set your username here 20 | 'pw' : 'mypw' // Set your password here 21 | } 22 | ``` 23 | 24 | IFTTN uses [bunyan](https://www.npmjs.org/package/bunyan) for log-output. To have an easy readable output on your console, log output should be redirected to bunyan. If you don't have bunyan installed globally but (automatically) installed as dependency run the following command: 25 | ``` 26 | node start | ./node_modules/bunyan/bin/bunyan 27 | ``` 28 | 29 | Have a look at the bunyan documentation if you want to further define which log level you want to see. If you want the server constantly running in the background, you can use [forever](https://www.npmjs.org/package/forever). 30 | ## Port Forwarding 31 | If your if-this-then-node instance is behind a router, you have to make it accessible to the internet. For this you need to configure a port forwarding in your router. if-this-then-node runs on port 1337, so you have to expose port 1337 on your host to the internet. 32 | 33 | ## Test if IFTTN works 34 | To test whether if-this-then-node can be accessed successfully, you can visit http://:/ifttn/ 35 | 36 | Of course you have to replace by your hostname or ip address (the external!) and port by the port which should be forwarded to your if-this-then-node instance. You should see a message like "IFTTN - if-this-then-node Version x.x.x is up and running!". 37 | 38 | ## A word about security 39 | IFTTN has a very simple authentication mechanism built in using a user and a password. I strongly recommend to use some kind of reverse proxy in front of your if-this-then-node instance which is accessible using HTTPS. If you do that, please note that IFTTT does not accept self-signed certificates! 40 | 41 | # Configuration of IFTTT Recipe 42 | In IFTTT configure a new recipe with any trigger you like and Maker as action channel. 43 | ## URL 44 | In your recipe set the URL to your instance of IFTTN, which could look like: 45 | ``` 46 | http://myifttnserver.anydns.info:1337/ifttn/ 47 | ``` 48 | By default this application runs on port 1337 and you might have to configure a port redirect in your router to make the instance of NodeJS accessible from the internet. See above. 49 | 50 | ## Method 51 | if-this-then-node only accepts request sent as POST request. So you have to select "POST" here. 52 | 53 | ## Content Type 54 | We're sending JSON requests, so choose "application/json". 55 | 56 | ## Body 57 | This is the most crucial part. You have to send a JSON structure, which contains some information, which is always required, as well as some additional information. 58 | 59 | Let's have a look at this sample for the limitless-zone-onoff plugin: 60 | ```javascript 61 | { 62 | "action": "limitless-zone-onoff", // always required 63 | "user": "myuser", // always required 64 | "pw": "mypassword", // always required 65 | "host": "192.168.178.24", 66 | "port": "8899", 67 | "zone": "3", 68 | "onoff": "on" 69 | } 70 | ``` 71 | As you can see, there are 3 parameters which are always required. That is the username and password you just configured in your config.js and the "action", which is the name of the plug in, you wish to execute. 72 | 73 | All other parameters are plugin-dependent. Please check the documentation of the specific plugins. 74 | 75 | Now the action should be all set up and you're able to trigger it. 76 | 77 | # Plugins 78 | Plugins are used for implementing new commands or actions into IFTTN. 79 | 80 | ## Available plugins 81 | ### wakeonlan-linux - Wake On Lan (from Linux systems) 82 | This plugin can be used to wake up a PC which supports Wake On Lan. This plugin depends on "wakeonlan". You might also use etherwake, but that needs to be implemented, yet. The following parameters need to be set in your action-configuration in IFTTT: 83 | * __broadcast__ - The broadcast address of the system you're trying to wake up. If it has the local IP 192.168.1.1 the broadcast address usually is 192.168.1.255 84 | * __mac__ - The MAC address of the interface which is beeing accessed written as 00:00:00:00:00:00 85 | 86 | #### Sample Request Body for IFTTT Maker Recipe 87 | A sample request body would look like this: 88 | 89 | ```javascript 90 | { 91 | "action": "wakeonlan-linux", 92 | "user": "myuser", 93 | "pw": "mypassword", 94 | "broadcast": "192.168.1.255", 95 | "mac": "00:00:00:00:00:00" 96 | } 97 | ``` 98 | 99 | ### windows-shutdown-linux - Shutdown Windows from Linux 100 | Does exactly what it says it does. It shuts down a Windows PC from a Linux system. It requires a local user profile with administrative privileges on the Windows system and the ``samba-common`` package to be installed on the Linux system executing the command. On some systems you might need to first uninstall samba-common first and reinstall it before the net-command is available. Run: 101 | ``` 102 | sudo apt-get remove samba-common 103 | sudo apt-get install samba-common 104 | ``` 105 | The following parameters need to be set in your action-configuration in IFTTT: 106 | * __ip__ - The local IP address of the system 107 | * __user__ - The username of the administrative user on the target system 108 | * __pw__ - The password of the administrative user on the target system 109 | 110 | 111 | #### Sample Request Body for IFTTT Maker Recipe 112 | A sample request body would look like this: 113 | 114 | ```javascript 115 | { 116 | "action": "wakeonlan-linux", 117 | "user": "myuser", 118 | "pw": "mypassword", 119 | "ip": "192.168.1.255", 120 | "windowsuser": "windows-username", 121 | "windowspw": "windows-password" 122 | } 123 | ``` 124 | ### LimitlessLED 125 | The "LimitlessLED" plugins can be used to control any LimitlessLED (also known as "MiLight" or "IWY Light" system using IFTTT. 126 | 127 | More information can be found [here](http://www.wifiledlamp.com/service/applamp-api/), [here](http://www.limitlessled.com/dev/) and [here](http://www.msxfaq.de/lync/impresence/iwylight.htm). 128 | 129 | #### limitless-zone-onoff 130 | An easy plugin for remotely turning on or off the lights of a specified zone. 131 | 132 | The following parameters need to be set in your action-configuration in IFTTT: 133 | * __host__ - The local IP address of your MiLight WIFI-Gateway 134 | * __port__ - The UDP port your MiLight-WIFI-Gateway is listening on 135 | * __zone__ - The number of the zone you wish to control. Numbers 1-4 usually. 136 | * __onoff__ - Whether to turn your zone on or off. Values: on, off 137 | 138 | ```javascript 139 | { 140 | "action": "limitless-zone-onoff", // always required 141 | "user": "myuser", // always required 142 | "pw": "mypassword", // always required 143 | "host": "192.168.178.24", 144 | "port": "8899", 145 | "zone": "3", 146 | "onoff": "on" 147 | } 148 | ``` 149 | 150 | #### limitless-onleave-autooff 151 | This plugin turns off your lights if all known clients have left. You can use this plugin together with IFTTT's geofencing features. Please note: if you are living with multiple people, each of them might need to have IFTTT running on their mobile phones and have this recipe set up and running. This is due to the fact, that the plugin needs to run a list of "accepted" clients in order to know when the last person has left and the lights can finally be turned off. 152 | 153 | This plugin requires redis. On Ubuntu/Debian/Respbian it can be installed like this: 154 | ``` 155 | sudo apt-get install redis-server 156 | ``` 157 | 158 | The following parameters need to be set in your action-configuration in IFTTT: 159 | * __host__ - The local IP address of your MiLight WIFI-Gateway 160 | * __port__ - The UDP port your MiLight-WIFI-Gateway is listening on 161 | * __clientname__ - The name of the client this recipe is running for 162 | * __enterexit__ - {{EnteredOrExited}} - IFTTT's ingredient to tell whether someone has entered or exited the geofence 163 | 164 | ```javascript 165 | { 166 | "action": "limitless-zone-onoff", // always required 167 | "user": "myuser", // always required 168 | "pw": "mypassword", // always required 169 | "host": "192.168.178.24", 170 | "port": "8899", 171 | "clientname": "myclientname", 172 | "enterexit": "{{EnteredOrExited}}" // this is an ingredient from IFTTT 173 | } 174 | ``` 175 | 176 | 177 | ## Writing Plugins 178 | Just have a look at the [sample plugin](https://github.com/sebauer/if-this-then-node/blob/master/plugins/sample-plugin.js) inside the plugins-directory, it should be pretty self-explaining. The most important thing is that EVERY plugin must at least implement the functions info() and run(params, log, callback). 179 | 180 | The "params" variable holds all parameters from IFTTT as an object. Using the example of our WOL-plugin this object would look like: 181 | ```javascript 182 | { 183 | 'broadcast': '192.168.1.255', 184 | 'mac': '00:00:00:00:00:00' 185 | } 186 | ``` 187 | 188 | The log parameter is a reference to the instance of the logger (bunyan) which allows you to log custom messages: 189 | ```javascript 190 | log.info('Foo %s', bar); 191 | ``` 192 | 193 | The callback parameter holds the callback function executed from IFTTN. It expects an result object as parameter: 194 | ```javascript 195 | { 196 | 'success': true, 197 | 'output': 'your output text here' 198 | } 199 | ``` 200 | 201 | Please note, that you MUST call the callback in your plugin as IFTTN would cannot send a response back to IFTTT. This would make IFTTT wait for a response until a timeout is reached and might result in your recipe being disabled after several failures. 202 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | 'user': 'myuser', 3 | 'pw': 'mypw' 4 | }; 5 | 6 | module.exports = { 7 | getConfig: function () { 8 | var returnObj = { 9 | 'user': config.user, 10 | 'pw': config.pw 11 | }; 12 | return returnObj; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /modules/helper.js: -------------------------------------------------------------------------------- 1 | var config = require('../config').getConfig(); 2 | var pjson = require('../package.json'); 3 | var log = null; 4 | 5 | module.exports = { 6 | getVersion: function () { 7 | return pjson.version; 8 | }, 9 | setLogger: function (logger) { 10 | log = logger; 11 | }, 12 | checkConfig: function () { 13 | // Validate that the user has set custom authentication details 14 | if (config.user === 'myuser' || config.pw === 'mypw') { 15 | log.error('Authentication details are still on their default values! Please set a custom username and password in config.js!'); 16 | process.exit(42); 17 | } 18 | }, 19 | printStartupHeader: function () { 20 | console.log('\n========================================================='); 21 | console.log('Starting "IFTTN - If This Then Node" Version ' + pjson.version); 22 | console.log('========================================================='); 23 | console.log('http://sebauer.github.io/if-this-then-node/'); 24 | console.log('---------------------------------------------------------\n'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /modules/parameter-extractor.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extractParameters: function (params) { 3 | var returnObj = {}; 4 | 5 | var user = params.param[1].value[0].string[0]; 6 | var pw = params.param[2].value[0].string[0]; 7 | var content = params.param[3].value[0].struct[0].member; 8 | 9 | // Now extract the required information from the POST content 10 | var action = content[1].value[0].string[0]; 11 | var categories = content[2].value[0].array[0].data[0].value; 12 | var actionParams = []; 13 | 14 | // Extract the parameters, faked as categories 15 | for (var i in categories) { 16 | var paramString = categories[i].string[0]; 17 | 18 | // Extract parameters to key/value pairs 19 | var extractedParams = paramString.match(/^([^\=]+)\=([^\=]+)$/); 20 | 21 | if (extractedParams === null) { 22 | throw new Error('Parameters not valid!'); 23 | } 24 | // Save extracted parameters 25 | actionParams[extractedParams[1]] = extractedParams[2]; 26 | } 27 | 28 | returnObj = { 29 | 'user': user, 30 | 'pw': pw, 31 | 'action': action, 32 | 'actionParams': actionParams 33 | }; 34 | return returnObj; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /modules/plugin-manager.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = { 4 | pluginList: [], 5 | log: {}, 6 | setLogger: function (logger) { 7 | this.log = logger; 8 | }, 9 | loadPlugins: function () { 10 | var regexPattern = /\.js$/i; 11 | 12 | this.log.info('Searching for plugins...'); 13 | var fileList = fs.readdirSync('./plugins/'); 14 | 15 | for (var i in fileList) { 16 | if (fileList[i].match(regexPattern)) { 17 | // Get the clear plugin name 18 | var pluginName = fileList[i].replace(regexPattern, ''); 19 | 20 | // Now load the plugin 21 | this.log.info('Loading %s', pluginName); 22 | this.pluginList[pluginName] = require('../plugins/' + pluginName); 23 | 24 | // Call the sample method to verify the plugin is working 25 | this.log.info(' >> %s', this.pluginList[pluginName].info()); 26 | } 27 | } 28 | }, 29 | execute: function (params, callback) { 30 | // Finally run our plugin with the parameters 31 | this.log.info('Executing plugin %s with params:', params.action); 32 | this.log.info(params); 33 | this.pluginList[params.action].run(params, this.log, callback); 34 | }, 35 | pluginExists: function (pluginName) { 36 | return typeof this.pluginList[pluginName] !== 'undefined'; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /modules/response-generator.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | failure: function (status, res, log) { 3 | 4 | log.info('Sending failure response with status code %d', status); 5 | // TODO create xml by using xml2js 6 | var xml = '\nfaultCode' + status + 'faultStringRequest was not successful.'; 7 | 8 | res.set({ 9 | 'Content-Type': 'text/xml' 10 | }); 11 | 12 | res.send(200, xml); 13 | }, 14 | success: function (status, res, log) { 15 | log.info('Sending success response "%s"', status); 16 | // TODO create xml by using xml2js 17 | var xml = '\n'; 18 | xml += '' + status + ''; 19 | 20 | res.set({ 21 | 'Content-Type': 'text/xml' 22 | }); 23 | 24 | res.send(200, xml); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /modules/xml-rpc-api-handler.js: -------------------------------------------------------------------------------- 1 | var responseGenerator = require('./response-generator'); 2 | var config = require('../config').getConfig(); 3 | var pluginManager = null; 4 | var log = null; 5 | 6 | module.exports = { 7 | setPluginManager: function (pm) { 8 | pluginManager = pm; 9 | }, 10 | setLogger: function (logger) { 11 | log = logger; 12 | }, 13 | handleRequest: function (req, res) { 14 | var params = req.body; 15 | 16 | // Validate user credenials 17 | if (params.user !== config.user || params.pw !== config.pw) { 18 | log.error('Authentication failed!'); 19 | responseGenerator.failure(401, res, log); 20 | return; 21 | } 22 | 23 | // See if we know this plugin and then execute it with the given parameters 24 | if (pluginManager.pluginExists(params.action)) { 25 | pluginManager.execute(params, function (result) { 26 | if (result.success === true) { 27 | log.info('Plugin succeeded with output %s', result.output); 28 | responseGenerator.success('200', res, log); 29 | } else { 30 | log.info('Plugin failed with output %s', result.output); 31 | responseGenerator.failure(1337, res, log); 32 | } 33 | }); 34 | } else { 35 | log.error('No plugin found for action %s', params.action); 36 | res.send(404, 'No plugin found for action ' + params.action); 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "if-this-then-node", 3 | "description": "An extendible NodeJS app to receive actions from IFTTT (If This Then That).", 4 | "version": "2.0.1", 5 | "private": true, 6 | "dependencies": { 7 | "express": "3.x", 8 | "xml2js": "0.4.x", 9 | "express-xml-bodyparser": "0.0.4", 10 | "bunyan": "0.x.x", 11 | "limitless-gem": "0.3.0", 12 | "redis": "0.11.x" 13 | }, 14 | "devDependencies": { 15 | "mocha" : "x.x.x", 16 | "fakeredis": "x.x.x" 17 | }, 18 | "scripts": { 19 | "test": "node ./node_modules/mocha/bin/mocha ./test/test.js --reporter spec" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugins/limitless-onleave-autooff.js: -------------------------------------------------------------------------------- 1 | var led = require('limitless-gem'); 2 | var redis = require('redis'); 3 | var format = require('util').format; 4 | var setName = 'ifttn-limitless-clients'; 5 | var client = null; 6 | 7 | module.exports = { 8 | setClient: function (redisClient) { 9 | client = redisClient; 10 | }, 11 | changeSetName: function (newName) { 12 | setName = newName; 13 | }, 14 | run: function (params, log, callback) { 15 | 16 | if (client === null) { 17 | client = redis.createClient(); 18 | } 19 | 20 | // Connect to our MiLight WIFI Gateway 21 | // TODO move to seperate module 22 | var connection = led.createSocket({ 23 | host: params.host, 24 | port: params.port 25 | }, 26 | 'udp', 27 | function () { 28 | log.info('Connected to LimitlessLED %s:%d', params.host, params.port); 29 | }); 30 | 31 | // If a client enters the geofence, register him within the redis keystore 32 | if (params.enterexit.toLowerCase() === 'entered' || params.enterexit.toLowerCase() === 'connected to') { 33 | log.info('Client %s is coming home', params.clientname); 34 | registerClient(params.clientname, function () { 35 | callback({ 36 | 'success': true, 37 | 'output': 'Client registered at home' 38 | }); 39 | }, log); 40 | // If a client leaves the geofence, remove him from the redis keystore and additionally check whether other 41 | // clients are still left. If yes, we're fine, if not, we have to switch off the lights 42 | } else if (params.enterexit.toLowerCase() === 'exited' || params.enterexit.toLowerCase() === 'disconnected from') { 43 | log.info('"%s" has left the building', params.clientname); 44 | deregisterClient(params.clientname, function (remainingClients) { 45 | var output = ''; 46 | if (remainingClients === 0) { 47 | output = 'All clients left, ALL lights turned OFF'; 48 | log.info(output); 49 | connection.send(led.RGBW.ALL_OFF); 50 | } else { 51 | output = 'Client deregistered, still clients at home, lights will stay on'; 52 | log.info('%d clients still at home, not switching off lights', remainingClients); 53 | } 54 | callback({ 55 | 'success': true, 56 | 'output': output 57 | }); 58 | }, log); 59 | } 60 | }, 61 | info: function () { 62 | return 'IFTTN LimitlessLED Plugin - onLeave: auto-off'; 63 | } 64 | }; 65 | 66 | var registerClient = function (clientname, callback, log) { 67 | 68 | if (client === null) { 69 | client = redis.createClient(); 70 | } 71 | 72 | // Add client to redis store 73 | log.info('Registered %s as being home', clientname); 74 | client.sadd(setName, clientname, function () { 75 | callback(); 76 | }); 77 | }; 78 | 79 | var deregisterClient = function (clientname, callback, log) { 80 | 81 | if (client === null) { 82 | redis.createClient(); 83 | } 84 | 85 | // Remove client from store 86 | client.srem(setName, clientname, function (err, reply) { 87 | // Now check if there are some clients left 88 | log.info('Checking for remaining clients..'); 89 | client.smembers(setName, function (err, replies) { 90 | // Execute callback with the number of remaining clients 91 | callback(replies.length); 92 | }); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /plugins/limitless-zone-onoff.js: -------------------------------------------------------------------------------- 1 | /* global log */ 2 | var led = require('limitless-gem'); 3 | 4 | module.exports = { 5 | run: function (params, log, callback) { 6 | 7 | var connection = led.createSocket({ 8 | host: params.host, 9 | port: params.port 10 | }, 11 | 'udp', 12 | function () { 13 | log.info('Connected to LimitlessLED %s:%d', params.host, params.port); 14 | } 15 | ); 16 | var cmd = switchLED(connection, params.zone, params.onoff); 17 | callback({ 18 | 'success': true, 19 | 'output': 'Sent command ' + cmd 20 | }); 21 | }, 22 | info: function () { 23 | return 'IFTTN LimitlessLED Plugin - Zone x On/Off'; 24 | } 25 | }; 26 | 27 | var switchLED = function (connection, zone, onoff) { 28 | var cmd = ''; 29 | switch (onoff.toUpperCase()) { 30 | case ('ON'): 31 | cmd = led.RGBW['GROUP' + zone + '_ON']; 32 | break; 33 | case ('OFF'): 34 | cmd = led.RGBW['GROUP' + zone + '_OFF']; 35 | break; 36 | default: 37 | log.warn('Error with command, input %s invalid', onoff); 38 | break; 39 | } 40 | connection.send(cmd); 41 | return cmd; 42 | }; 43 | -------------------------------------------------------------------------------- /plugins/sample-plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | run: function (params, log, callback) { 3 | // do whatever you want in this plugin 4 | callback({ 5 | 'success': true, 6 | 'output': 'all good!' 7 | }); 8 | }, 9 | info: function () { 10 | return 'IFTTN Sample Plugin Version 1.0'; 11 | } 12 | }; 13 | 14 | // This private 15 | var anyHelperFunction = function () { 16 | } 17 | -------------------------------------------------------------------------------- /plugins/wakeonlan-linux.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | module.exports = { 4 | run: function (params, log, callbackFunction) { 5 | var command = 'wakeonlan -i ' + params.broadcast + ' ' + params.mac; 6 | log.info(command); 7 | exec(command, function (error, stdout, stderr) { 8 | if (error != null) { 9 | callbackFunction({ 10 | 'success': false, 11 | 'output': error 12 | }); 13 | } else { 14 | callbackFunction({ 15 | 'success': true, 16 | 'output': stdout 17 | }); 18 | } 19 | }); 20 | }, 21 | info: function () { 22 | return 'IFTTN Wake-On-LAN Plugin (Linux) V0.1'; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /plugins/win-shutdown-linux.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | module.exports = { 4 | run: function (params, log, callbackFunction) { 5 | var command = 'net rpc shutdown -I ' + params.ip + ' -U ' + params.windowsuser + '%' + params.windowspw; 6 | log.info(command); 7 | exec(command, function (error, stdout, stderr) { 8 | if (error != null) { 9 | callbackFunction({ 10 | 'success': false, 11 | 'output': stderr 12 | }); 13 | } else { 14 | callbackFunction({ 15 | 'success': true, 16 | 'output': stdout 17 | }); 18 | } 19 | }); 20 | }, 21 | info: function () { 22 | return 'IFTTN Shutdown Windows from Linux'; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IFTTT to NodeJS 3 | * 4 | * Use this little Node app to have a node server running which can be accessed 5 | * by IFTTT's WordPress action. I needed this to have an easy way of using IFTTT 6 | * to communicate with my Raspberry PI. 7 | * 8 | * Credits for the idea of faking the Wordpress XML RPC API as customizable 9 | * IFTTT backend go to https://github.com/captn3m0/ifttt-webhook 10 | * 11 | */ 12 | var express = require('express'); 13 | var xmlparser = require('express-xml-bodyparser'); 14 | var bunyan = require('bunyan'); 15 | 16 | var helper = require('./modules/helper'); 17 | 18 | helper.printStartupHeader(); 19 | 20 | var pluginManager = require('./modules/plugin-manager'); 21 | var responseGenerator = require('./modules/response-generator'); 22 | var xmlRpcApiHandler = require('./modules/xml-rpc-api-handler'); 23 | 24 | var config = require('./config.js').getConfig(); 25 | 26 | var log = bunyan.createLogger({name: 'IFTTN'}); 27 | var app = express(); 28 | 29 | app.use(express.json()); 30 | app.use(express.urlencoded()); 31 | app.use(xmlparser()); 32 | 33 | helper.setLogger(log); 34 | helper.checkConfig(); 35 | 36 | pluginManager.setLogger(log); 37 | pluginManager.loadPlugins(); 38 | 39 | xmlRpcApiHandler.setLogger(log); 40 | xmlRpcApiHandler.setPluginManager(pluginManager); 41 | 42 | // Middleware to log every request 43 | app.use(function (req, res, next) { 44 | log.info('%s from %s on %s', req.method, req.ip, req.path); 45 | next(); 46 | }); 47 | 48 | app.get('/ifttn/', function (req, res, next) { 49 | res.send('IFTTN - if-this-then-node Version ' + helper.getVersion() + ' is up and running!'); 50 | }); 51 | 52 | app.post('/ifttn/', function (req, res, next) { 53 | log.info('Request received'); 54 | 55 | xmlRpcApiHandler.handleRequest(req, res); 56 | }); 57 | 58 | var server = app.listen(1337, function () { 59 | log.info('Listening on port %d', server.address().port); 60 | }); 61 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, afterEach, before */ 2 | var assert = require('assert'); 3 | var parseString = require('xml2js').parseString; 4 | var redis = require('fakeredis'); 5 | var parameterExtractor = require('../modules/parameter-extractor.js'); 6 | var pluginManager = require('../modules/plugin-manager.js'); 7 | var limitlessZoneOnOff = require('../plugins/limitless-zone-onoff'); 8 | var limitlessOnleaveAutooff = require('../plugins/limitless-onleave-autooff'); 9 | var led = require('limitless-gem'); 10 | 11 | // Lets fake a logger 12 | var logMock = { 13 | 'info': function () {}, 14 | 'debug': function () {}, 15 | 'warn': function () {}, 16 | 'error': function () {} 17 | }; 18 | 19 | describe('Limitless LED Plugins', function () { 20 | 21 | describe('limitless-zone-onoff', function () { 22 | describe('when switching a Zone ON', function () { 23 | it('should send the command to switch the specified zone ON', function (done) { 24 | limitlessZoneOnOff.run({ 25 | 'zone': '1', 26 | 'onoff': 'on' 27 | }, logMock, function (result) { 28 | assert.equal('Sent command ' + led.RGBW['GROUP1_ON'], result.output); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | describe('when switching a Zone OFF', function () { 34 | it('should send the command to switch the specified zone ON', function (done) { 35 | limitlessZoneOnOff.run({ 36 | 'zone': '3', 37 | 'onoff': 'off' 38 | }, logMock, function (result) { 39 | assert.equal('Sent command ' + led.RGBW['GROUP3_OFF'], result.output); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('limitless-onleave-autooff', function () { 47 | 48 | var redisSetName = 'unittest-runner'; 49 | var client = redis.createClient(); 50 | limitlessOnleaveAutooff.changeSetName(redisSetName); 51 | limitlessOnleaveAutooff.setClient(client); 52 | 53 | afterEach(function (done) { 54 | client.del(redisSetName, done); 55 | }); 56 | 57 | describe('on exiting', function () { 58 | before(function (done) { 59 | client.sadd(redisSetName, ['foo', 'bar'], done); 60 | }); 61 | 62 | it('should correctly remove the client from redis', function (done) { 63 | limitlessOnleaveAutooff.run({ 64 | 'clientname': 'foo', 65 | 'enterexit': 'exited' 66 | }, logMock, function () { 67 | client.smembers(redisSetName, function (err, replies) { 68 | assert.equal(1, replies.length); 69 | assert.equal('bar', replies[0]); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('on last client exiting', function () { 77 | before(function (done) { 78 | client.sadd(redisSetName, 'foobar', done); 79 | }); 80 | 81 | it('should remove the last client and turn off the lights', function (done) { 82 | limitlessOnleaveAutooff.run({ 83 | 'clientname': 'foobar', 84 | 'enterexit': 'exited' 85 | }, logMock, function (result) { 86 | assert.equal('All clients left, ALL lights turned OFF', result.output); 87 | client.smembers(redisSetName, function (err, replies) { 88 | assert.equal(0, replies.length); 89 | done(); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | describe('on entering', function () { 96 | it('Should correctly add clients to redis', function (done) { 97 | var clientName = 'unittest'; 98 | 99 | limitlessOnleaveAutooff.run({ 100 | 'clientname': clientName, 101 | 'enterexit': 'entered' 102 | }, logMock, function () { 103 | client.sismember(redisSetName, clientName, function (err, reply) { 104 | assert.equal(1, reply); 105 | done(); 106 | }); 107 | }); 108 | }); 109 | }); 110 | }); 111 | }); 112 | 113 | describe('Plugin Manager', function () { 114 | describe('Plugin Loader', function () { 115 | it('Should find all plugins', function (done) { 116 | pluginManager.setLogger(logMock); 117 | pluginManager.loadPlugins(); 118 | assert.equal(5, Object.keys(pluginManager.pluginList).length); 119 | done(); 120 | }); 121 | }); 122 | }); 123 | 124 | describe('XML Parsing', function () { 125 | var xmlString = 'metaWeblog.newPostmeinusermeinpwtitleTitledescriptionwakeonlancategoriesbroadcast=192.168.178.255mac=00:25:22:A2:84:8Dmt_keywordstag1post_statuspublish1'; 126 | var xmlStringBadParams = 'metaWeblog.newPostmeinusermeinpwtitleTitledescriptionwakeonlancategoriesbroadcast=192.=168.178.255mac=00:25:22:A2:84:8Dmt_keywordstag1post_statuspublish1'; 127 | 128 | describe('Parameter Extractor', function () { 129 | it('Should extract all parameters correctly', function (done) { 130 | parseString(xmlString, { 131 | explicitArray: true, 132 | normalize: true, 133 | normalizeTags: true, 134 | trim: true 135 | }, 136 | function (err, result) { 137 | var params = parameterExtractor.extractParameters(result.methodcall.params[0]); 138 | assert.equal('meinuser', params.user); 139 | assert.equal('meinpw', params.pw); 140 | assert.equal('wakeonlan', params.action); 141 | assert.deepEqual({ 142 | 'broadcast': '192.168.178.255', 143 | 'mac': '00:25:22:A2:84:8D' 144 | }, params.actionParams); 145 | done(); 146 | }); 147 | }); 148 | it('Should throw an exception when the parameters are incorrectly set', function (done) { 149 | parseString(xmlStringBadParams, { 150 | explicitArray: true, 151 | normalize: true, 152 | normalizeTags: true, 153 | trim: true 154 | }, 155 | function (err, result) { 156 | assert.throws(function () { 157 | parameterExtractor.extractParameters(result.methodcall.params[0]); 158 | }, Error, 'Parameters not valid!'); 159 | done(); 160 | }); 161 | }); 162 | }); 163 | }); 164 | --------------------------------------------------------------------------------