howto

Controlling IKEA Tradfri devices from your computer

IKEA is cheap and everywhere

tags
zigbee
IKEA
node

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

  1. Go to IKEA and buy stuff
  2. Setup IKEA Trådfri Gateway and Lights as normal
  3. Install the node-tradfri-client library
  4. 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:

  1. https://learn.pimoroni.com/tutorial/sandyj/controlling-ikea-tradfri-lights-from-your-pi
  2. https://github.com/AlCalzone/node-tradfri-client

Previously

howto

Playing with cabal

serverless code

tags
p2p
node
cabal

Next

howto

Reverse engineering APIs using Chrome Developer Tools

its your data after all

tags
chrome
api
scraping