I stumbled upon a fun blogpost about the Dumbass Home 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.
Overview
- Go to IKEA and buy stuff
- Setup IKEA Trådfri Gateway and Lights as normal
- Install the
node-tradfri-client
library - Copy the below scripts
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.
Example code
We are going to use the node-tradfri-client library, the delay library, and the conf node library to store values after the fun.
1
2
3
4
| mkdir ikeatest
cd ikeatest
npm init
yarn add node-tradfri-client delay conf
|
Find the Gateway
gateway.js
:
1
2
3
| const tradfri = require( 'node-tradfri-client' )
tradfri.discoverGateway().then( result => console.log( result ) )
|
Getting a security token
Look at the back of your gateway to get the security token. We will use this to get an
access token to the gateway, which we will then use to communicate with the device.
Set the IKEASECURITY
token in the environment and then run this script:
1
| $ export IKEASECURITY=akakakak
|
connection.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| const Conf = require('conf');
const delay = require('delay');
const NodeTradfriClient = require("node-tradfri-client");
const path = require( 'path' );
const conf = new Conf();
const { discoverGateway, TradfriClient } = NodeTradfriClient;
async function getConnection() {
console.log( "Looking up IKEA Tradfri gateway on your network" )
let gateway = await discoverGateway()
if( gateway == null ) {
console.log( "No Tradfri gateway found in local network" );
process.exit(1);
}
console.log( "Connecting to", gateway.host)
const tradfri = new TradfriClient(gateway.host)
if( !conf.has( 'security.identity' ) || !conf.has('security.psk' ) ) {
let securityCode = process.env.IKEASECURITY
if( securityCode === "" || securityCode === undefined ) {
console.log( "Please set the IKEASECURITY env variable to the code on the back of the gateway")
process.exit(1)
}
console.log( "Getting identity from security code" )
const {identity, psk} = await tradfri.authenticate(securityCode);
conf.set( 'security', {identity,psk} )
}
console.log( "Securely connecting to gateway" )
await tradfri.connect(conf.get( 'security.identity' ), conf.get( 'security.psk' ))
return tradfri;
}
module.exports = {getConnection: getConnection};
// Only run this method if invoked with "node connection.js"
if( __filename === process.argv[1] ) {
(async () => {
const tradfri = await getConnection();
console.log( "Connection complete" )
console.log( "Waiting 1 second")
await delay( 1000 )
console.log( "Closing connection")
tradfri.destroy()
process.exit(0);
})()
}
|
Printing out discovered device info
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.
devices.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| const connection = require( './connection' );
const delay = require( 'delay' );
function printDeviceInfo( device ) {
switch( device.type ) {
case 0: // remote
case 4: // sensor
console.log( device.instanceId, device.name, `battery ${device.deviceInfo.battery}%` )
break;
case 2: // light
let lightInfo = device.lightList[0]
let info = {
onOff: lightInfo.onOff,
spectrum: lightInfo.spectrum,
dimmer: lightInfo.dimmer,
color: lightInfo.color,
colorTemperature: lightInfo.colorTemperature
}
console.log( device.instanceId, device.name, lightInfo.onOff ? "On" : "Off", JSON.stringify( info) )
break;
case 3: // plug
console.log( device.instanceId, device.name, device.plugList[0].onOff ? "On" : "Off" )
break;
default:
console.log( device.instanceId, device.name, "unknown type", device.type)
console.log( device )
}
}
function findDevice( tradfri, deviceNameOrId ) {
let lowerName = deviceNameOrId.toLowerCase();
for( const deviceId in tradfri.devices ) {
if( deviceId === deviceNameOrId ) {
return tradfri.devices[deviceId];
}
if( tradfri.devices[deviceId].name.toLowerCase() === lowerName ) {
return tradfri.devices[deviceId];
}
}
return;
}
module.exports = {printDeviceInfo, findDevice};
// Only run this method if invoked with "node devices.js"
if( __filename === process.argv[1] ) {
(async () => {
const tradfri = await connection.getConnection();
tradfri.observeDevices();
// Wait a second hopefully something will be loaded by then!
await delay( 1000 )
for (const deviceId in tradfri.devices ) {
const device = tradfri.devices[deviceId];
printDeviceInfo( device )
}
tradfri.destroy()
process.exit(0);
})()
}
|
Registering our own device listeners
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!
device_watcher.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| const connection = require( './connection' );
const devices = require( './devices' );
function deviceUpdated( device ) {
devices.printDeviceInfo( device );
}
function deviceRemoved( deviceId ) {
console.log( "See you later", deviceId, "it's been great.")
}
(async () => {
const tradfri = await connection.getConnection();
tradfri
.on("device updated", deviceUpdated)
.on("device removed", deviceRemoved)
.observeDevices();
})()
|
Switching and dimming
Now that we have code that can react to changes, lets write some code that controls things!
device_changer.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| const connection = require( './connection' );
const devices = require( './devices' );
const delay = require( 'delay' );
(async () => {
let argv = process.argv;
if( argv.length <= 2 ) {
console.log( "Usage:" )
console.log( "node device_changer.js", "deviceId", "--on")
console.log( "node device_changer.js", "deviceId", "--off")
console.log( "node device_changer.js", "deviceId", "--toggle")
console.log( "node device_changer.js", "deviceId", "--color hexcolor")
console.log( "node device_changer.js", "deviceId", "--brightness 0-100")
process.exit(1)
}
const tradfri = await connection.getConnection();
tradfri.observeDevices();
await delay( 1000 )
let position = 2;
let currentDevice = null;
let accessory = null;
while( position < argv.length ) {
switch( argv[position] ) {
case '--on':
console.log( "Turning", currentDevice.instanceId, "on")
accessory.turnOn()
break;
case '--off':
console.log( "Turning", currentDevice.instanceId, "off")
accessory.turnOff();
break;
case '--toggle':
accessory.toggle();
console.log( "toggle device", currentDevice.instanceId )
break;
case '--color':
position++;
console.log( "Setting color of", currentDevice.instanceId, "to", argv[position])
accessory.setColor(argv[position])
break;
case '--brightness':
position++;
console.log( "Setting brightness of", currentDevice.instanceId, "to", argv[position])
accessory.setBrightness( argv[position] )
break;
default:
currentDevice = devices.findDevice( tradfri, argv[position] )
if( currentDevice == null ) {
console.log( "Unable to find device", argv[position] )
console.log( tradfri.devices )
process.exit(1)
}
switch( currentDevice.type ) {
case 0:
case 4:
console.log( "Can't control this type of device" )
process.exit(1);
break;
case 2: //light
accessory = currentDevice.lightList[0]
accessory.client = tradfri
break;
case 3: // plug
accessory = currentDevice.plugList[0]
accessory.client = tradfri
break;
}
break;
}
position ++;
}
await delay(1000);
await tradfri.destroy();
process.exit(0);
})()
|
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:
1
| node device_changer.js 65538 --on --color efd275 65543 --brightness 50 --on 65540 --on
|
Scenes and Rooms
The library also has methods to deal with scenes and rooms all at once. Let’s take a look at a room watcher:
scenes.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| const connection = require( './connection' );
const delay = require( 'delay' );
const devices = require( './devices')
function printRoomInfo( tradfri, room ) {
const group = room.group
const scenes = room.scenes
console.log( "ROOM", group.instanceId, room.name, "Current Scene:", scenes[group.sceneId].name )
console.log( "DEVICES")
for( const deviceId of group.deviceIDs ) {
devices.printDeviceInfo( tradfri.devices[deviceId] )
}
console.log( "SCENES" )
for (const sceneId in scenes ) {
const scene = scenes[sceneId]
console.log( sceneId, scene.name ) // , scene.lightSettings )
}
console.log( "----\n")
}
function findRoom( tradfri, name ) {
let lowerName = name.toLowerCase();
// Look for the group
for (const groupId in tradfri.groups ) {
if( tradfri.groups[groupId].group.name.toLowerCase() === lowerName ) {
return tradfri.groups[groupId];
}
if( groupId === name ) {
return tradfri.groups[groupId];
}
}
return null;
}
module.exports = {printRoomInfo, findRoom};
// Only run this method if invoked with "node devices.js"
if( __filename === process.argv[1] ) {
(async () => {
const tradfri = await connection.getConnection();
tradfri.observeDevices();
tradfri.observeGroupsAndScenes();
// Wait a second hopefully something will be loaded by then!
await delay( 1500 )
for (const groupId in tradfri.groups ) {
const collection = tradfri.groups[groupId];
printRoomInfo( tradfri, collection )
}
tradfri.destroy()
process.exit(0);
})()
}
|
Setting the scene
Lets write another small utility to be able to change a room to a preset setting!
scene_changer.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| const connection = require( './connection' );
const devices = require( './devices' );
const scenes = require( './scenes');
const delay = require( 'delay' );
(async () => {
let argv = process.argv;
if( argv.length != 4 ) {
console.log( "Usage:" )
console.log( "node scene_changer.js", "room", "scene")
process.exit(1)
}
const tradfri = await connection.getConnection();
tradfri.observeDevices();
tradfri.observeGroupsAndScenes();
await delay( 1500 )
const roomName = argv[2]
let sceneName = argv[3]
const room = scenes.findRoom( tradfri, roomName );
if( room == null ) {
console.log( "Unable to find room named", roomName);
process.exit(1);
}
let scene = null;
sceneName = sceneName.toLowerCase();
// Look for the scene
for (const sceneId in room.scenes ) {
if( room.scenes[sceneId].name.toLowerCase() === sceneName ) {
scene = room.scenes[sceneId]
}
}
if( scene == null ) {
console.log( "Unable to find scene named", sceneName )
process.exit(1);
}
room.group.client = tradfri;
scenes.printRoomInfo( tradfri, room )
console.log( "Switching", room.group.name, "to scene", scene.name )
room.group.activateScene(scene.instanceId);
scenes.printRoomInfo( tradfri, room )
// Give the messages a chance to propogate
await delay(1000);
await tradfri.destroy();
process.exit(0);
})()
|
Other stuff
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.
Have fun playing around!
References:
- https://learn.pimoroni.com/tutorial/sandyj/controlling-ikea-tradfri-lights-from-your-pi
- https://github.com/AlCalzone/node-tradfri-client