├── .gitignore ├── src ├── gateway.ts ├── device_watcher.ts ├── scene_changer.ts ├── connection.ts ├── scenes.ts ├── devices.ts └── device_changer.ts ├── tsconfig.json ├── package.json ├── README.md ├── .eslintrc └── index.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | .local_git 4 | key.json 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /src/gateway.ts: -------------------------------------------------------------------------------- 1 | import tradfri from "node-tradfri-client"; 2 | 3 | tradfri.discoverGateway().then((result: tradfri.DiscoveredGateway | null) => console.log(result)); 4 | -------------------------------------------------------------------------------- /src/device_watcher.ts: -------------------------------------------------------------------------------- 1 | import { Accessory } from "node-tradfri-client"; 2 | import { getConnection } from "./connection"; 3 | import { printDeviceInfo } from "./devices"; 4 | 5 | function deviceUpdated(device: Accessory): void { 6 | printDeviceInfo(device); 7 | } 8 | 9 | function deviceRemoved(instanceId: number): void { 10 | console.log("See you later", instanceId, "it's been great."); 11 | } 12 | 13 | (async () => { 14 | const tradfri = await getConnection(); 15 | tradfri 16 | .on("device updated", deviceUpdated) 17 | .on("device removed", deviceRemoved) 18 | .observeDevices(); 19 | })(); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", // ~node10 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2015", 7 | "es2016", 8 | "es2017", 9 | "es2018" 10 | ], 11 | "declaration": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | "outDir": "./bin", 15 | "rootDir": "./src", 16 | "strict": true, 17 | "esModuleInterop": true, 18 | "preserveConstEnums": true, 19 | "skipLibCheck": true, 20 | "resolveJsonModule": true, 21 | "types": [ 22 | "node" 23 | ] 24 | }, 25 | "include": [ 26 | "src/" 27 | ], 28 | "exclude": [ 29 | "**/*.spec.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "controlling_ikea_tradfri_with_node", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "device_info.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint src/**", 9 | "build": "tsc" 10 | }, 11 | "assert": false, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "conf": "^4.0.1", 16 | "delay": "^4.2.0", 17 | "node-tradfri-client": "^2.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^14.14.37", 21 | "typescript": "^4.2.3", 22 | "eslint": "^7.22.0", 23 | "@typescript-eslint/eslint-plugin": "^4.18.0", 24 | "@typescript-eslint/parser": "^4.18.0", 25 | "eslint-plugin-jest": "^24.3.1", 26 | "jest": "^26.6.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Check out https://willschenk.com/articles/2019/controlling_ikea_tradfri_with_node/ for more information. 2 | 3 | ## Basic usage: 4 | 5 | Look at the back of your gateway and find the security code. We use that for the initial challenge and then store the token in a configuration file. 6 | 7 | Create key.json with your security code 8 | 9 | ```json 10 | { 11 | "secret": "ABC123...." 12 | } 13 | ``` 14 | 15 | then 16 | 17 | ```bash 18 | yarn install 19 | yarn build 20 | ``` 21 | 22 | ### Scripts are located in the created "lib" folder 23 | 24 | ## Scripts: 25 | 26 | 1. `node bin/devices.js` - Connects, prints devices it knows about, and quits 27 | 2. `node bin/device_watcher.js` - Watches for changes as they happen, waits forever 28 | 3. `node bin/device_changer.js "Bulb 1" --on --color efd275 "Bulb 2" --brightness 50 --on "Plug" --on` -- Makes changes to specific devices, add as many as you want to the list. 29 | 4. `node bin/scenes.js` -- List out rooms and availabe scenes 30 | 5. `node bin/scene_changer.js "Room 1" relax` -- Switches room 1 to the `relax` scene 31 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/eslint-recommended", 6 | "plugin:@typescript-eslint/recommended", // uses the recommended rules from the @typescript-eslint/eslint-plugin 7 | "plugin:jest/recommended" // enables eslint-plugin-jest 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module" 12 | }, 13 | "ignorePatterns": [ 14 | "lib/", 15 | "src/*.json" 16 | ], 17 | "rules": { 18 | "quotes": ["error", "double"], 19 | "indent": ["error", 2, { "SwitchCase": 1 }], 20 | "linebreak-style": ["error", "unix"], 21 | "semi": ["error", "always"], 22 | 23 | "comma-dangle": ["error", "always-multiline"], 24 | "dot-notation": "error", 25 | "eqeqeq": ["error", "always", {"null": "ignore"}], 26 | "curly": ["error", "all"], 27 | "brace-style": ["error"], 28 | 29 | "@typescript-eslint/no-non-null-assertion": "off" // currently disabled, hap-nodejs has some bad typing (like getCharacteristic) for this to be enabled 30 | } 31 | } -------------------------------------------------------------------------------- /src/scene_changer.ts: -------------------------------------------------------------------------------- 1 | import { getConnection } from "./connection"; 2 | import delay from "delay"; 3 | import { findRoom, printRoomInfo } from "./scenes"; 4 | 5 | (async () => { 6 | const argv = process.argv; 7 | 8 | if( argv.length !== 4 ) { 9 | console.log( "Usage:" ); 10 | console.log( "node scene_changer.js", "room", "scene"); 11 | 12 | process.exit(1); 13 | } 14 | 15 | const tradfri = await getConnection(); 16 | tradfri.observeDevices(); 17 | tradfri.observeGroupsAndScenes(); 18 | await delay( 1500 ); 19 | 20 | const roomName = argv[2]; 21 | let sceneName = argv[3]; 22 | 23 | const room = findRoom( tradfri, roomName ); 24 | 25 | if( room == null ) { 26 | console.log( "Unable to find room named", roomName); 27 | process.exit(1); 28 | } 29 | 30 | let scene = null; 31 | 32 | sceneName = sceneName.toLowerCase(); 33 | // Look for the scene 34 | for (const sceneId in room.scenes ) { 35 | if( room.scenes[sceneId].name.toLowerCase() === sceneName ) { 36 | scene = room.scenes[sceneId]; 37 | } 38 | } 39 | 40 | if( scene == null ) { 41 | console.log( "Unable to find scene named", sceneName ); 42 | process.exit(1); 43 | } 44 | 45 | //room.group.client = tradfri; 46 | printRoomInfo( tradfri, room ); 47 | 48 | console.log( "Switching", room.group.name, "to scene", scene.name ); 49 | room.group.activateScene(scene.instanceId); 50 | 51 | printRoomInfo( tradfri, room ); 52 | 53 | // Give the messages a chance to propogate 54 | await delay(1000); 55 | tradfri.destroy(); 56 | process.exit(0); 57 | })(); 58 | -------------------------------------------------------------------------------- /src/connection.ts: -------------------------------------------------------------------------------- 1 | import key from "./key.json"; 2 | import Conf from "conf"; 3 | import delay from "delay"; 4 | import { DiscoveredGateway, discoverGateway, TradfriClient } from "node-tradfri-client"; 5 | 6 | const conf = new Conf({projectName: "tradfri-cli"}); 7 | 8 | async function getConnection(): Promise { 9 | console.log("Looking up IKEA Tradfri gateway on your network"); 10 | const gateway: DiscoveredGateway | null = await discoverGateway(); 11 | 12 | if(gateway === undefined || gateway === null) { 13 | console.log( "No Tradfri gateway found in local network" ); 14 | process.exit(1); 15 | } 16 | 17 | console.log( "Connecting to", gateway.host); 18 | const tradfri = new TradfriClient(gateway.host as string); 19 | 20 | if( !conf.has( "security.identity" ) || !conf.has("security.psk" ) ) { 21 | const securityCode = key.secret; 22 | if( securityCode === "" || securityCode === undefined ) { 23 | console.log( "Please set the IKEASECURITY env variable to the code on the back of the gateway"); 24 | process.exit(1); 25 | } 26 | 27 | console.log( "Getting identity from security code" ); 28 | const {identity, psk} = await tradfri.authenticate(securityCode); 29 | 30 | conf.set("security", { identity, psk }); 31 | } 32 | 33 | console.log("Securely connecting to gateway"); 34 | await tradfri.connect(conf.get("security.identity") as string, conf.get("security.psk") as string); 35 | 36 | return tradfri; 37 | } 38 | 39 | export {getConnection}; 40 | 41 | // Only run this method if invoked with "node connection.js" 42 | if( __filename === process.argv[1] ) { 43 | (async () => { 44 | const tradfri = await getConnection(); 45 | console.log( "Connection complete" ); 46 | 47 | console.log( "Waiting 1 second"); 48 | await delay( 1000 ); 49 | 50 | console.log( "Closing connection"); 51 | tradfri.destroy(); 52 | process.exit(0); 53 | })(); 54 | } 55 | -------------------------------------------------------------------------------- /src/scenes.ts: -------------------------------------------------------------------------------- 1 | import delay from "delay"; 2 | import { Group, GroupInfo, Scene, TradfriClient } from "node-tradfri-client"; 3 | import { getConnection } from "./connection"; 4 | import { printDeviceInfo } from "./devices"; 5 | 6 | function printRoomInfo( tradfri: TradfriClient, room: GroupInfo ): void { 7 | const group: Group = room.group; 8 | const scenes: Record = room.scenes; 9 | 10 | console.log( "ROOM", group.instanceId, group.name); 11 | console.log( "DEVICES"); 12 | for(const deviceId of group.deviceIDs) { 13 | printDeviceInfo( tradfri.devices[deviceId] ); 14 | } 15 | console.log( "SCENES" ); 16 | for (const sceneId in scenes ) { 17 | const scene = scenes[sceneId]; 18 | console.log( sceneId, scene.name ); // , scene.lightSettings ) 19 | } 20 | 21 | console.log( "----\n"); 22 | 23 | } 24 | 25 | function findRoom( tradfri: TradfriClient, name: string ): GroupInfo | null { 26 | const lowerName = name.toLowerCase(); 27 | 28 | // Look for the group 29 | for (const groupId in tradfri.groups ) { 30 | if( tradfri.groups[groupId].group.name.toLowerCase() === lowerName ) { 31 | return tradfri.groups[groupId]; 32 | } 33 | 34 | if( groupId === name ) { 35 | return tradfri.groups[groupId]; 36 | } 37 | } 38 | 39 | return null; 40 | } 41 | 42 | export {printRoomInfo, findRoom}; 43 | 44 | // Only run this method if invoked with "node devices.js" 45 | if( __filename === process.argv[1] ) { 46 | (async () => { 47 | const tradfri = await getConnection(); 48 | 49 | tradfri.observeDevices(); 50 | tradfri.observeGroupsAndScenes(); 51 | 52 | // Wait a second hopefully something will be loaded by then! 53 | await delay(1500); 54 | 55 | for (const groupId in tradfri.groups ) { 56 | const collection = tradfri.groups[groupId]; 57 | printRoomInfo( tradfri, collection ); 58 | } 59 | 60 | tradfri.destroy(); 61 | process.exit(0); 62 | })(); 63 | } 64 | -------------------------------------------------------------------------------- /src/devices.ts: -------------------------------------------------------------------------------- 1 | import {getConnection} from "./connection"; 2 | import delay from "delay"; 3 | import { Accessory, Light, TradfriClient } from "node-tradfri-client"; 4 | 5 | function printDeviceInfo( device: Accessory ): void { 6 | switch( device.type ) { 7 | case 0: // remote 8 | case 4: // sensor 9 | console.log( device.instanceId, device.name, `battery ${device.deviceInfo.battery}%` ); 10 | break; 11 | case 2: { // light 12 | const lightInfo: Light = device.lightList[0]; 13 | const info = { 14 | onOff: lightInfo.onOff, 15 | spectrum: lightInfo.spectrum, 16 | dimmer: lightInfo.dimmer, 17 | color: lightInfo.color, 18 | colorTemperature: lightInfo.colorTemperature, 19 | }; 20 | console.log( device.instanceId, device.name, lightInfo.onOff ? "On" : "Off", JSON.stringify( info) ); 21 | break; 22 | } 23 | case 3: // plug 24 | console.log( device.instanceId, device.name, device.plugList[0].onOff ? "On" : "Off" ); 25 | break; 26 | default: 27 | console.log( device.instanceId, device.name, "unknown type", device.type); 28 | console.log( device ); 29 | } 30 | } 31 | 32 | function findDevice( tradfri: TradfriClient, deviceNameOrId: string): Accessory | undefined { 33 | const lowerName = deviceNameOrId.toLowerCase(); 34 | 35 | for( const deviceId in tradfri.devices ) { 36 | if( deviceId === deviceNameOrId ) { 37 | return tradfri.devices[deviceId]; 38 | } 39 | 40 | if( tradfri.devices[deviceId].name.toLowerCase() === lowerName ) { 41 | return tradfri.devices[deviceId]; 42 | } 43 | } 44 | 45 | return undefined; 46 | } 47 | 48 | export {printDeviceInfo, findDevice}; 49 | 50 | // Only run this method if invoked with "node devices.js" 51 | if( __filename === process.argv[1] ) { 52 | (async () => { 53 | const tradfri = await getConnection(); 54 | 55 | tradfri.observeDevices(); 56 | 57 | // Wait a second hopefully something will be loaded by then! 58 | await delay( 1000 ); 59 | 60 | for (const deviceId in tradfri.devices ) { 61 | const device = tradfri.devices[deviceId]; 62 | printDeviceInfo( device ); 63 | } 64 | 65 | tradfri.destroy(); 66 | process.exit(0); 67 | })(); 68 | } 69 | -------------------------------------------------------------------------------- /src/device_changer.ts: -------------------------------------------------------------------------------- 1 | import {getConnection} from "./connection"; 2 | import delay from "delay"; 3 | import { findDevice } from "./devices"; 4 | import { Accessory } from "node-tradfri-client"; 5 | 6 | (async () => { 7 | const argv = process.argv; 8 | 9 | if( argv.length <= 2 ) { 10 | console.log( "Usage:" ); 11 | console.log( "node device_changer.js", "deviceId", "--on"); 12 | console.log( "node device_changer.js", "deviceId", "--off"); 13 | console.log( "node device_changer.js", "deviceId", "--toggle"); 14 | console.log( "node device_changer.js", "deviceId", "--color hexcolor"); 15 | console.log( "node device_changer.js", "deviceId", "--brightness 0-100"); 16 | 17 | process.exit(1); 18 | } 19 | 20 | const tradfri = await getConnection(); 21 | tradfri.observeDevices(); 22 | await delay(1000); 23 | 24 | let position = 2; 25 | let currentDevice: Accessory | undefined = undefined; 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | let accessory: any | null = null; 28 | 29 | while( position < argv.length ) { 30 | switch( argv[position] ) { 31 | case "--on": 32 | console.log( "Turning", currentDevice?.instanceId, "on"); 33 | accessory?.turnOn(); 34 | break; 35 | case "--off": 36 | console.log( "Turning", currentDevice?.instanceId, "off"); 37 | accessory?.turnOff(); 38 | break; 39 | case "--toggle": 40 | accessory?.toggle(); 41 | console.log( "toggle device", currentDevice?.instanceId ); 42 | break; 43 | case "--color": 44 | position++; 45 | console.log( "Setting color of", currentDevice?.instanceId, "to", argv[position]); 46 | accessory?.setColor(argv[position]); 47 | break; 48 | case "--brightness": 49 | position++; 50 | console.log( "Setting brightness of", currentDevice?.instanceId, "to", argv[position]); 51 | accessory?.setBrightness(Number(argv[position])); 52 | break; 53 | default: 54 | currentDevice = findDevice( tradfri, argv[position] ); 55 | if( currentDevice == null ) { 56 | console.log( "Unable to find device", argv[position] ); 57 | console.log( tradfri.devices ); 58 | process.exit(1); 59 | } 60 | switch(currentDevice.type) { 61 | case 0: 62 | case 4: 63 | console.log( "Can't control this type of device" ); 64 | process.exit(1); 65 | break; 66 | case 2: //light 67 | accessory = currentDevice.lightList[0]; 68 | accessory.client = tradfri; 69 | break; 70 | case 3: // plug 71 | accessory = currentDevice.plugList[0]; 72 | accessory.client = tradfri; 73 | break; 74 | } 75 | break; 76 | } 77 | 78 | position ++; 79 | } 80 | 81 | await delay(1000); 82 | tradfri.destroy(); 83 | process.exit(0); 84 | })(); 85 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Controlling IKEA Tradfri devices from your computer 3 | subtitle: IKEA is cheap and everywhere 4 | tags: 5 | - howto 6 | - zigbee 7 | - IKEA 8 | - node 9 | date: "2019-04-24" 10 | repository: https://github.com/wschenk/tradfri-cli 11 | remote: git@github.com:wschenk/tradfri-cli.git 12 | --- 13 | 14 | I stumbled upon a [fun blogpost about the Dumbass Home](https://vas3k.com/blog/dumbass_home/?ref=sn) and it turned me onto the IKEA Trådfri line of products. So I got a couple, and figured out how to control them from my laptop (or say a Raspberry PI) from node. Here's how to do it. 15 | 16 | 17 | 18 | ## Overview 19 | 20 | 1. Go to IKEA and buy stuff 21 | 2. Setup IKEA Trådfri Gateway and Lights as normal 22 | 3. Install the `node-tradfri-client` library 23 | 4. Copy the below scripts 24 | 25 | First, set up a switch, lightbulb, and a gateway. The gateway needs to be plugged into the router which is a bit of a pain. You need at least one controller connected to a device to get the gateway to recognize things; once you have that it should be fairly straightforward. When in doubt, move closer to the gateway. 26 | 27 | ## Example code 28 | 29 | We are going to use the [node-tradfri-client library](https://github.com/AlCalzone/node-tradfri-client), the delay library, and the conf node library to store values after the fun. 30 | 31 | ```bash 32 | mkdir ikeatest 33 | cd ikeatest 34 | npm init 35 | yarn add node-tradfri-client delay conf 36 | ``` 37 | 38 | ## Find the Gateway 39 | 40 | [`gateway.js`](gateway.js): 41 | 42 | {{% code file="articles/2019/controlling_ikea_tradfri_with_node/gateway.js" language="js" %}} 43 | 44 | ## Getting a security token 45 | 46 | Look at the back of your gateway to get the security token. We will use this to get an 47 | access token to the gateway, which we will then use to communicate with the device. 48 | Set the `IKEASECURITY` token in the environment and then run this script: 49 | 50 | ```bash 51 | $ export IKEASECURITY=akakakak 52 | ``` 53 | 54 | [`connection.js`](connection.js): 55 | 56 | {{% code file="articles/2019/controlling_ikea_tradfri_with_node/connection.js" language="js" %}} 57 | 58 | ## Printing out discovered device info 59 | 60 | Calling the `observeDevices()` method will make the client start listening for devices that the gateway is connected to. The library itself keeps track of what it knows inside of the `tradfri.devices` hash, so we'll pause for a bit to give it time to listen and then print out what it found. 61 | 62 | [`devices.js`](devices.js): 63 | 64 | {{% code file="articles/2019/controlling_ikea_tradfri_with_node/devices.js" language="js" %}} 65 | 66 | ## Registering our own device listeners 67 | 68 | We can register a listener callback to watch for when thing change, keeping our program running forever watching for the lights to go on and off! 69 | 70 | [`device_watcher.js`](device_watcher.js): 71 | 72 | {{% code file="articles/2019/controlling_ikea_tradfri_with_node/device_watcher.js" language="js" %}} 73 | 74 | ## Switching and dimming 75 | 76 | Now that we have code that can react to changes, lets write some code that controls things! 77 | 78 | [`device_changer.js`](device_changer.js): 79 | 80 | {{% code file="articles/2019/controlling_ikea_tradfri_with_node/device_changer.js" language="js" %}} 81 | 82 | This lets you add multiple commands on the line, so if we wanted to make a few changes at once you could do something like this: 83 | 84 | ```bash 85 | node device_changer.js 65538 --on --color efd275 65543 --brightness 50 --on 65540 --on 86 | ``` 87 | 88 | ## Scenes and Rooms 89 | 90 | The library also has methods to deal with scenes and rooms all at once. Let's take a look at a room watcher: 91 | 92 | [`scenes.js`](scenes.js): 93 | 94 | {{% code file="articles/2019/controlling_ikea_tradfri_with_node/scenes.js" language="js" %}} 95 | 96 | ## Setting the scene 97 | 98 | Lets write another small utility to be able to change a room to a preset setting! 99 | 100 | [`scene_changer.js`](scene_changer.js): 101 | 102 | {{% code file="articles/2019/controlling_ikea_tradfri_with_node/scene_changer.js" language="js" %}} 103 | 104 | ## Other stuff 105 | 106 | You can also update the settings of the devices in the controller, which we aren't going to cover. You can also add additional scenes and update them. These are documented further in the fantastic library. 107 | 108 | Have fun playing around! 109 | 110 | --- 111 | 112 | References: 113 | 114 | 1. https://learn.pimoroni.com/tutorial/sandyj/controlling-ikea-tradfri-lights-from-your-pi 115 | 2. https://github.com/AlCalzone/node-tradfri-client 116 | --------------------------------------------------------------------------------