├── .gitignore ├── README.md ├── app.js ├── get_key.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## IKEA Tradfri IFTTT Starter Guide 2 | 3 | This project is based on [google_home_starter](https://github.com/krpeacock/google_home_starter) by Kyle Peacock. 4 | 5 | Using this project you can controll your IKEA lights using the Google Home/Assistant. The names of the lightbulbs and groups should be set up in the IKEA app. As there is no way to make subgroups, you can hack around it by naming lighbulbs in a similar fashion. For example: 6 | * Have a group called `Living Room`. 7 | * Name your lightbulbs `Couch 1`, `Couch 2`, `Lamp 1`. 8 | * You can then tell the assistant to `turn all the living room lights`. 9 | * Or `turn on the couch lights` which is a subgroup of the living room lights. 10 | * The command will try to match the group name exactly. 11 | * If no match is found, it will apply the action to all bulbs that start with the given argument. 12 | 13 | ### Getting Started 14 | If you haven't already, install git and node.js on your device. 15 | 16 | 1. Fork or clone this repository onto your device. 17 | 2. In your console, run `npm install` to install the required components. 18 | 3. Run `touch .env` to create your hidden, gitignored environment config file. 19 | 4. Generate a preshared key for your application by running `node get_key.js` 20 | * Input the IP to the IKEA Tradfri hub 21 | * Input the security code printed on the HUB 22 | * You will get back the identity and psk values to be saved into the .env file 23 | 5. In .env, configure your environment as follows: 24 | * `DEV=` `TRUE` if you are on desktop, or `FALSE` if you are on your raspberry pi 25 | * `PORT=` The port that the server will listen on. 26 | * `PASS=` Whatever you want the password to your API to be 27 | * `HUBIP=` The IP to your IKEA Tradfri hub 28 | * `APIUSER=` The user/identity used to the connect to the hub 29 | * `APIKEY=` The password/psk used to connect to the hub 30 | 6. Run `npm start` to launch the server 31 | 32 | 7. Test your API 33 | * The API has the following structure: `http://[IPADDRESS]:[PORT]/api/:operation/:what/:state?password=[PASS]` 34 | * `[IPADDRESS]` - the internet accessible IP address used to access the API. This is usually the public IP address of your router. 35 | * `[PORT]` - the internet accessible PORT used to access the API. You usually have to forward this from your router to the host running this package. 36 | * `[PASS]` - the password you defined in the .env file 37 | * You can test that it works by making calls with `curl` such as `curl -i -X POST http://[IPADDRESS]:[PORT]/api/turn/all/on?password=[PASS]` (this should turn on all the lights) 38 | 39 | 7. Setup the IFTTT commands: 40 | * Create a new applet 41 | * For `this` use `Google Assistant` 42 | * For `that` use `webhook` 43 | * Create the following applets: 44 | - `Turn the $ lights on` => `http://[IPADDRESS]:[PORT]/api/turn/{{TextField}}/on?password=[PASS]` 45 | - `Turn the $ lights off` => `http://[IPADDRESS]:[PORT]/api/turn/{{TextField}}/off?password=[PASS]` 46 | - `Set the $ lights to #` => `http://[IPADDRESS]:[PORT]/api/dim/{{TextField}}/{{NumberField}}?password=[PASS]` 47 | - `Set the light color to $` => `http://[IPADDRESS]:[PORT]/api/color/all/{{TextField}}?password=[PASS]` 48 | - `Set the $ light temperature to #` => `http://[IPADDRESS]:[PORT]/api/temp/{{TextField}}/{{NumberField}}?password=[PASS]` 49 | 50 | 8. Run the project at startup: 51 | * Add a line similar to `cd /home/pi/node_tradfri_ifttt && nohup npm start &` to your `/etc/rc.local` 52 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const fs = require('fs'); 4 | const express = require('express'); 5 | const bodyParser= require('body-parser'); 6 | const path = require('path') 7 | const app = express(); 8 | 9 | const tradfriLib = require("node-tradfri-client"); 10 | const nodeCleanup = require('node-cleanup'); 11 | const RGBColor = require('rgbcolor'); 12 | 13 | nodeCleanup(function (exitCode, signal) { 14 | console.log("Cleaning up..."); 15 | if (tradfri) { 16 | console.log("Destroying tradfri connection"); 17 | tradfri.destroy(); 18 | } 19 | tradfri = undefined; 20 | }); 21 | 22 | const TradfriClient = tradfriLib.TradfriClient; 23 | var tradfri = new TradfriClient(process.env.HUBIP); 24 | 25 | app.post('/api/:command/:id/:state', function(req, res) { 26 | if (req.query.password != process.env.PASS) { 27 | console.log("invalid password"); 28 | res.status(403).send("wrong password"); 29 | return; 30 | } 31 | 32 | var command = req.params.command; 33 | if (command == "turn" || 34 | command == "dim" || 35 | command == "temp" || 36 | command == "color") { 37 | executeCommand(req.params.id, command, req.params.state); 38 | res.send("done"); 39 | return; 40 | } 41 | 42 | console.log("unknown command", command); 43 | res.status(404).send("wrong command"); 44 | }); 45 | 46 | function performOperation(bulb, command, state) 47 | { 48 | console.log(command, bulb.name, "(" + bulb.instanceId + ")", state); 49 | if (command == "turn") { 50 | tradfri.operateLight(bulb, {onOff: state == "on"}); 51 | } else if (command == "dim") { 52 | tradfri.operateLight(bulb, {dimmer: state}); 53 | } else if (command == "temp") { 54 | tradfri.operateLight(bulb, {colorTemperature: state}); 55 | } else if (command == "color") { 56 | // Fixup color names into rgb value strings if necessary 57 | var color = new RGBColor(state); 58 | state = color.toHex().slice(1); 59 | tradfri.operateLight(bulb, {color: state}); 60 | } 61 | } 62 | 63 | function executeCommand(id, command, state) { 64 | // When the id gets passed as "the living room" turn it into "living room" 65 | // Also "all the" gets turned into all. 66 | id = id.replace(/the/g, "").trim().toLowerCase(); 67 | 68 | // If it's just "all" then we set it to the empty string, so we fall back 69 | // to setting every bulb (because every bulb name starts with empty string) 70 | if (id == "all") { 71 | id = ""; 72 | } 73 | 74 | console.log("executeCommand", command, id, state); 75 | 76 | for (var groupId in groups) { 77 | var group = groups[groupId]; 78 | if (group.name.toLowerCase() == id) { 79 | tradfri.operateGroup(group, {onOff: state == "on"}); 80 | for (var deviceId of group.deviceIDs) { 81 | var bulb = lightbulbs[deviceId]; 82 | if (bulb) { // skip non-bulbs 83 | performOperation(bulb, command, state); 84 | } 85 | } 86 | return; 87 | } 88 | } 89 | 90 | for (var bulbid in lightbulbs) { 91 | var bulb = lightbulbs[bulbid]; 92 | if (bulb.name.toLowerCase().startsWith(id)) { 93 | performOperation(bulb, command, state); 94 | // we don't return, so we can apply to all bulbs that share a naming convention 95 | } 96 | } 97 | } 98 | 99 | const lightbulbs = {}; 100 | function tradfri_deviceUpdated(device) { 101 | console.log("tradfri_deviceUpdated", device.instanceId, device.name) 102 | if (device.type === tradfriLib.AccessoryTypes.lightbulb) { 103 | // remember it 104 | lightbulbs[device.instanceId] = device; 105 | } 106 | } 107 | 108 | function tradfri_deviceRemoved(instanceId) { 109 | if (instanceId in lightbulbs) { 110 | console.log("tradfri_deviceRemoved", instanceId, lightbulbs[instanceId].name) 111 | delete lightbulbs[instanceId]; 112 | } 113 | } 114 | 115 | const groups = {}; 116 | function tradfri_groupUpdated(group) { 117 | // remember it 118 | console.log("tradfri_groupUpdated", group.instanceId, group.name) 119 | groups[group.instanceId] = group; 120 | } 121 | 122 | app.listen(process.env.PORT, function() { 123 | console.log('Listening on port ' + process.env.PORT); 124 | tradfri.connect(process.env.APIUSER, process.env.APIKEY) 125 | .then(() => { 126 | tradfri.on("device updated", tradfri_deviceUpdated) 127 | .on("device removed", tradfri_deviceRemoved) 128 | .observeDevices(); 129 | tradfri.on("group updated", tradfri_groupUpdated) 130 | .observeGroupsAndScenes(); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /get_key.js: -------------------------------------------------------------------------------- 1 | const tradfriLib = require("node-tradfri-client"); 2 | const TradfriClient = tradfriLib.TradfriClient; 3 | const nodeCleanup = require('node-cleanup'); 4 | 5 | nodeCleanup(function (exitCode, signal) { 6 | console.log("Cleaning up..."); 7 | if (tradfri) { 8 | console.log("Destroying tradfri connection"); 9 | tradfri.destroy(); 10 | } 11 | tradfri = undefined; 12 | }); 13 | 14 | var stdin = process.openStdin(); 15 | var tradfri; 16 | stdin.addListener("data", function(d) { 17 | // note: d is an object, and when converted to a string it will 18 | // end with a linefeed. so we (rather crudely) account for that 19 | // with toString() and then trim() 20 | var input = d.toString().trim(); 21 | console.log("you entered: [" + input + "]"); 22 | if (state == 0) { 23 | tradfri = new TradfriClient(input); 24 | console.log("input the security code printed on the gateway"); 25 | state = 1; 26 | } else { 27 | tradfri.authenticate(input) 28 | .then(result => { 29 | console.log(result); 30 | process.exit(0); }); 31 | } 32 | }); 33 | 34 | var state = 0; 35 | console.log("input the gateway IP"); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tradfri_ifttt", 3 | "version": "1.0.0", 4 | "description": "Backend to control IKEA TRADFRI from IFTTT", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/valenting/node_tradfri_ifttt.git" 12 | }, 13 | "keywords": [ 14 | "raspberry", 15 | "pi", 16 | "diy", 17 | "node.js", 18 | "ifttt", 19 | "ikea", 20 | "tradfri", 21 | "google home", 22 | "google assistant" 23 | ], 24 | "author": "Valentin Goșu", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/valenting/node_tradfri_ifttt/issues" 28 | }, 29 | "homepage": "https://github.com/valenting/node_tradfri_ifttt#readme", 30 | "dependencies": { 31 | "body-parser": "^1.16.0", 32 | "dotenv": "^4.0.0", 33 | "express": "^4.14.0", 34 | "fs": "0.0.1-security", 35 | "node-cleanup": "^2.1.2", 36 | "node-tradfri-client": "^0.8.6", 37 | "path": "^0.12.7", 38 | "rgbcolor": "^1.0.1" 39 | }, 40 | "devDependencies": { 41 | "locus": "^1.2.0" 42 | } 43 | } 44 | --------------------------------------------------------------------------------