Controlling IKEA Tradfri devices from your computer

IKEA is cheap and everywhere

Published April 24, 2019 #howto #zigbee #IKEA #node

The code from this article can be found at https://github.com/wschenk/tradfri-cli

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.

mkdir ikeatest
cd ikeatest
npm init
yarn add node-tradfri-client delay conf

Find the Gateway

gateway.js:

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:

$ export IKEASECURITY=akakakak

connection.js:

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()

  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:

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:

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:

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:

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:

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:

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
The code from this article can be found at https://github.com/wschenk/tradfri-cli

Read next

See also

Playing with cabal

serverless code

Cabal is a “experimental p2p community chat platform”. It’s fully distributed over the dat protocol. When you create a new chat area – something like a Slack – it allows anyone with the same key to post and view messages everywhere. When you post a message, everyone gets it and shares it with everyone else, so even when your computer drops off there will still be a coherent view of the data.

Read more

Splitting Git Repos and Work Directories

all the fun things git can do

I found a tutorial on how to manage your dotfiles, that works by splitting up the git repository (normally the .git directory) from the work directory. Since I have a lot of code that I put in my tutorials, I adapted the technique to have individual article directories mirrored in their own github repository. Repositories and Work Directories The normal usage of git is to type git clone <remote> to get a copy of the local directory, mess with stuff, and then add and commit your changes.

Read more

Setting up Indieweb Homepage

the dream of the nineties is alive on the indieweb

Remember microformats? Me neither! Back when the web was open and we were trying to find ways to interconnect independent things? Let’s bring them back!

Read more