TRADFRI Lチカ
こちらに面白い記事があった。
IKEA Tradfri Gatewayがいつの間にか売られていたので、Node.jsでド派手なLチカをしてみる。
nodeとモジュールだけでゲートウェイにアクセスして、LEDを制御できそうな雰囲気。 面白そうなので試してみよう。
ラズパイにnodejsを準備。では作業用フォルダ作成。
mkdir tradfri
そして、移動
cd tradfri
まずはモジュールダウンロード。IKEAのGW向け モジュールが作られているようだ。
npm install node-tradfri-client
これで、node_modulesフォルダが出来て、何かが入った。 では、サンプルソースをコピペしよう。実際には少し勉強がてら、内容をいじった。
// モジュールrequire
var tradfri = require("node-tradfri-client");
// セキュリティコード
var SECURITY_CODE = [※ゲートウェイ裏面のセキュリティコード※];
// クライアントインスタンス生成用
var Client = tradfri.TradfriClient
// 処理関数
async function lightTo(deviceName, onOff) {
// gateway検索
const gateway = await tradfri.discoverGateway();
// console.log('Find gateway:' + JSON.stringify(gateway));
// client作成
var client = new Client(gateway.addresses[0]);
// SECURITY_CODEでトークン取得
const {identity, psk} = await client.authenticate(SECURITY_CODE);
// 接続
await client.connect(identity, psk);
// デバイス参照
client.on("device updated", async (device) => {
// console.log('device:' + JSON.stringify(device));
if (device.name === deviceName && device.type === tradfri.AccessoryTypes.lightbulb) {
// 対象デバイスの場合、ライトを制御
await client.operateLight(device, {onOff, transitionTime:5});
// 後片付け処理
setTimeout(() => client.destroy(), 500);
}
}).observeDevices();
}
// 処理実行
// LED電球On
lightTo([※IKEAアプリでのデバイス名※], true);4行目のセキュリティコードは、ゲートウェイ裏面に見ずらい文字で記載されているコード。 よく見ないとわかりにくい。 32行目のデバイス名は、イケアトロードフリアプリでのデバイス名。 不明時には20行目のコメントを外して、適当なデバイス名で実行すると、デバイス一覧がログに出てくるので、 ここから拾う。そして、32行目の2つ目の引数が電球のOn(true)/Off(false)だ。
PC部屋と、電球のある部屋が別なのだが、「transitionTime:5」とあるせいか、実行してすぐ部屋を移動すると、 タイムラグのために、点灯するさまを見ることが出来る。ナイスなLチカだ。
なぜかうまくいっていた処理が、途中からエラーになった。「node-aead-crypto」が必要となった。どのタイミングでエラーになり始めたか不明だが、 とりあえず追加インストールしてやると動いた。
npm install node-aead-crypto
これも一緒に入れておくほうがよいだろう。
状態取得
IKEA電球とは双方向通信となるため、現在の状態を取得することも可能。 上記のclientでの「device updated」で現在の状態が返却されている様子。 電球としては、「lightList」でその内容がわかるようだ。nameが該当の電球に対するlightListを見てみる。
| キー | 内容 | 説明 |
|---|---|---|
| onOff | True/False | 電球の点灯状態 |
| dimmer | 0~100? | 電球の明るさ |
| color | ? | 電球の色 |
| colorTemp | ? | 電球の色温度 |
| colorX | ? | ? |
| colorY | ? | ? |
| isDimmable | True/False | 明るさ変化可否 |
| isSwitchable | True/False | オンオフ可否 |
| spectrum | none/white/rgb | 電球種類? |
| transitionTime | 秒指定 | 変化時間 |
では、これを拾ってみよう。 処理としては、上記関数の18行目「client.on(“device updated”」で取得した「device」 に内容が詰まっているので、
// デバイス参照
client.on("device updated", async (device) => {
// console.log('device:' + JSON.stringify(device));
if (device.name === deviceName && device.type === tradfri.AccessoryTypes.lightbulb) {
// 対象デバイスの場合、状態表示
console.log(device.lightList[0].onOff);
// 後片付け処理
setTimeout(() => client.destroy(), 500);
}
}).observeDevices();「対象デバイスの場合、状態表示」のコメント個所で状態をログに吐いて内容を見てみる。
API化(express)
色々操作できるようなので、APIサーバを作成してみよう。 expressを用意して、APIサーバを作成してみる。
mkdir tradfri
cd tradfri
npm init -y
素のプロジェクトが出来たら、expressを入れる。
npm install express –save
express-generatorはすでに入っているので、express-generatorを動かす。 入れてない場合には#「sudo npm install -g express-generator」でインストールしてから行う。
express –ejs
パラメータとしては、「–ejs」ではなく、「–view=ejs」が正解といったメッセージが出ていた。 パラメータが古いのかな?
ヘルプ見ると問題ないように見えるけど。まあ気にせず進める。
npm install
#エラー無くインストールが終われば完了。ひとまず動かしてみよう。
npm start
コンソールへのメッセージがしょぼいので、動作状態がよくわからないが、動いたっぽければ、ブラウザでアクセスしてみる。
raspberrypi.local:3000
初期画面が出てくればOKだ。Ctrl+Cで終わらせよう。

開発時はついでにnodemonも入れておく。ソース更新時に自動リロードしてくれるので、 いちいちサービスを停止、起動の手間が省ける。
npm install nodemon –save
起動は「npm start」ではなく
npx nodemon ./bin/www
で動かすようになるのだが、ついでにpackage.jsonも直して、
「npm start」でも同じ動作になるようにしておこう。
nano package.json
{
"name": "tradfri",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "npx nodemon ./bin/www" ※ここを修正
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"node-tradfri-client": "^2.1.8",
"nodemon": "^2.0.6"
}
}6行目の「start」箇所を書き換えて、nodemon動作にしておく。これで、普通に「npm start」することで、nodemon動作となる。
では、ソース修正を開始する。 まずは最初のapp.jsを修正。9行目にtradfri向けルーター作成
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var tradfriRouter = require('./routes/trd'); ※ここを追加そして、そこへ誘導する設定25行目にを追加。
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/trd', tradfriRouter); ※ここを追加「app.jsの修正はここまで。ほかの既存処理は触るのが面倒なので、そのまま残しておく。」
次にtradfri向けルーターの作成。
nano ./routes/trd.js
セキュリティコードとデバイス名はあらかじめ調べてセットしておく。
var express = require('express');
var router = express.Router();
// モジュールrequire
var tradfri = require("node-tradfri-client");
// セキュリティコード
var SECURITY_CODE = '※ゲートウェイのセキュリティコード※';
// クライアントインスタンス生成用
var Client = tradfri.TradfriClient
// 処理関数
async function lightTo(deviceName, onOff) {
// gateway検索
const gateway = await tradfri.discoverGateway();
// console.log('Find gateway:' + JSON.stringify(gateway));
// client作成
var client = new Client(gateway.addresses[0]);
// SECURITY_CODEでトークン取得
const {identity, psk} = await client.authenticate(SECURITY_CODE);
// 接続
await client.connect(identity, psk);
// デバイス参照
client.on("device updated", async (device) => {
// console.log('device:' + JSON.stringify(device));
if (device.name === deviceName && device.type === tradfri.AccessoryTypes.lightbulb) {
// 対象デバイスの場合、ライトを制御
await client.operateLight(device, {onOff, transitionTime:5});
// 後片付け処理
setTimeout(() => client.destroy(), 500);
}
}).observeDevices();
}
/* GET users listing. */
router.get('/andon/:mode', async function(req, res, next) {
const deviceName = '※デバイス名※';
var resMsg = 'tradfri api access[' + deviceName + ']';
if (req.params.mode.toLowerCase() === 'on') {
lightTo(deviceName, true);
} else if (req.params.mode.toLowerCase() === 'off') {
lightTo(deviceName, false);
}
res.send(resMsg);
});
module.exports = router;ひとまず完成。これで動かしてみよう。 照明をつけるときは、以下へアクセス。照明が点灯すればOK。
http://raspberrypi.local:3000/trd/andon/on
そして、消すときには以下へアクセス。
http://raspberrypi.local:3000/trd/andon/off
これで、点灯と消灯が出来ればOK。 うまくいかないときは、コメントアウトしてあるログ出力(console.log)を活かして、調査しよう。
状態取得も調べたので、これもできるようにしてみよう。
var express = require('express');
var router = express.Router();
// モジュールrequire
var tradfri = require("node-tradfri-client");
// セキュリティコード
var SECURITY_CODE = '※ゲートウェイのセキュリティコード※';
// クライアントインスタンス生成用
var Client = tradfri.TradfriClient
// 処理関数
async function lightTo(deviceName, onOff) {
// gateway検索
const gateway = await tradfri.discoverGateway();
// console.log('Find gateway:' + JSON.stringify(gateway));
// client作成
var client = new Client(gateway.addresses[0]);
// SECURITY_CODEでトークン取得
const {identity, psk} = await client.authenticate(SECURITY_CODE);
// 接続
await client.connect(identity, psk);
// デバイス参照
client.on("device updated", async (device) => {
// console.log('device:' + JSON.stringify(device));
if (device.name === deviceName && device.type === tradfri.AccessoryTypes.lightbulb) {
// 対象デバイスの場合、ライトを制御
await client.operateLight(device, {onOff, transitionTime:1});
// 後片付け処理
setTimeout(() => client.destroy(), 500);
}
}).observeDevices();
}
async function lightGet(deviceName) {
var result = '';
// gateway検索
const gateway = await tradfri.discoverGateway();
// console.log('Find gateway:' + JSON.stringify(gateway));
// client作成
var client = new Client(gateway.addresses[0]);
// SECURITY_CODEでトークン取得
const {identity, psk} = await client.authenticate(SECURITY_CODE);
// 接続
await client.connect(identity, psk);
// デバイス参照
await client.on("device updated", async (device) => {
if (device.name === deviceName && device.type === tradfri.AccessoryTypes.lightbulb) {
// 対象デバイスの場合、情報取得
// console.log('device:' + JSON.stringify(device));
result = device;
// 後片付け処理
setTimeout(() => client.destroy(), 500);
}
}).observeDevices();
return result;
}
/* GET access. */
async function lightGetAccess(req, deviceName) {
// コマンドに合わせてLight処理
var resMsg = 'tradfri api access[' + deviceName + ']';
if (req.params.mode.toLowerCase() === 'on') {
// 点灯
lightTo(deviceName, true);
resMsg += '<br>Light On';
} else if (req.params.mode.toLowerCase() === 'off') {
// 消灯
lightTo(deviceName, false);
resMsg += '<br>Light Off';
} else {
var ret = await lightGet(deviceName);
// 状態返却
// わかりやすく見える用
resMsg = '<pre>' + JSON.stringify(ret, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
// そのまま返却
// resMsg = ret;
}
return resMsg;
}
router.get('/andon/:mode', async function(req, res, next) {
// 「照明」向け処理
const deviceName = '※デバイス名※';
res.send(await lightGetAccess(req, deviceName));
});
module.exports = router;「get」にアクセスすることで、情報表示ができるようにした。
http://raspberrypi.local:3000/trd/andon/get

デーモン化
なんとなく動作するようになったので、デーモン化してみる。 nodemonで動かしておけば、ソース変更すると自動反映するので、デーモン動作中デモ更新が可能。
まずはサービスファイルを作成するのだが、ここで一つ確認が必要。 起動させるものはすべてフルパス指定が必要なので、npxのありかなどを調べておく必要がある。 たとえば「npx」の場所を調べるには
which npx
で場所を調べる。nvmでnodeを入れているので、nvm管理下の「/home/pi/.nvm/versions/node/v14.15.3/bin」に存在しているようだ。 しかし、なぜかこのままだとデーモン化しても、起動されない。
nvm下のnpxを/usr/local/binに移す
sudo ln -s “$(which node)” /usr/local/bin/node
sudo ln -s “$(which npm)” /usr/local/bin/npm
sudo ln -s “$(which npx)” /usr/local/bin/npx
これで、/usr/local/binで起動出来るようになる。 そのうえで、サービスファイルを作成する。
sudo nano /etc以下の/systemd/system/tradfri.service
内容は以下。
[Unit] Description =IKEA tradfri control Service App After=syslog.target [Service] ExecStart=/usr/local/bin/npx nodemon /home/pi/share/tradfri/bin/www WorkingDirectory=/home/pi/share/tradfri Restart=on-failure RestartSec=30 KillMode=control-group TimeoutStopSec=5 StandardOutput=file:/tmp/tradfri.log Type=simple [Install] WantedBy=multi-user.target
「StandardOutput」でテンポラリファイルを指定してあるので、標準出力はここに出力される。
ファイルが出来たら、権限などを上げておく。
sudo chown root:root /etc下の/systemd/system/tradfri.service
sudo chmod 744 /etc下の/systemd/system/tradfri.service
そして、デーモン管理をリロードして新たなサービスを認識してもらう。(やらなくても自動認識されるっぽいけど)
sudo systemctl daemon-reload
デーモン側の認識状態を確認。
systemctl list-unit-files –type=service | grep tradfri
tradfri.service disabled
表示されたから、デーモンとしての認識はされているようなので、起動してみる。
sudo systemctl start tradfri
そして、ログを確認
tail -f /var/log/syslog
実際には「journalctl」を見るのが正解のようだ。
journalctl -u tradfri -e -f
また、標準出力をファイル出力してあるので、通常の手動起動のような状態を見るのはこちら。
tail -f /tmp/tradfri.log

動作に問題ないようなら、常駐化しておこう。
sudo systemctl enable tradfri
これで、ラズパイ起動時に実行されるようになる。
明るさ変更
明るさが変更可能なようなので、実行してみる。on/off変更コマンドの「operateLight」メソッドで、 実行可能なようなので、明るさを変更する関数を用意。
async function lightDimmer(deviceName, setDimmer) {
// 明るさ変更
// gateway検索
const gateway = await tradfri.discoverGateway();
// client作成
var client = new Client(gateway.addresses[0]);
// SECURITY_CODEでトークン取得
const {identity, psk} = await client.authenticate(SECURITY_CODE);
// 接続
await client.connect(identity, psk);
// デバイス参照
client.on("device updated", async (device) => {
if (device.name === deviceName && device.type === tradfri.AccessoryTypes.lightbulb) {
// 対象デバイスの場合、ライトを制御
await client.operateLight(device, {dimmer: setDimmer, transitionTime: 3});
// 後片付け処理
setTimeout(() => client.destroy(), 500);
}
}).observeDevices();
}デバイスと明るさ(0~100)を渡せば、実行。実際に明るさが変化した。 該当デバイスに対して、「operateLight」メソッドで、[dimmer]に値を渡せばよいだけなので簡単だ。
外部呼出しで実行されるように、「lightGetAccess」関数に組み込む。 「dimmer」と値の指定で、その明るさになるよう指示可能。明るさは0~100になるよう、制御しておく。 あと、今後のデバッグ用にデバイス情報返却コマンドも用意。
/* GET access. */
async function lightGetAccess(req, deviceName) {
// コマンドに合わせてLight処理
let resMsg = '';
if (deviceName) {
resMsg = 'tradfri api access[' + deviceName + ']';
if (req.params.mode.toLowerCase() === 'on') {
// 点灯
lightTo(deviceName, true);
resMsg += '<br>Light On';
} else if (req.params.mode.toLowerCase() === 'off') {
// 消灯
lightTo(deviceName, false);
resMsg += '<br>Light Off';
} else if (req.params.mode.toLowerCase() === 'dimmer') { ※この辺りを追加
// 明るさ変更
let bright = 100;
if (req.params.tm) {
if (isFinite(req.params.tm)) {
bright = Number(req.params.tm)
if (bright > 100) bright = 100
}
}
// 明るさセット
lightDimmer(deviceName, bright);
resMsg += '<br>Light Dimmer ' + bright.toString();
} else {
var ret = await lightGet(deviceName);
// 状態返却
if (req.params.mode.toLowerCase() === 'pre') {
resMsg += '<pre>' + JSON.stringify(ret, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
} else {
resMsg += '"Device":' + JSON.stringify(ret);
}
}
} else {
// デバイス指定無しの場合は全情報返却
var result = '';
// gateway検索
const gateway = await tradfri.discoverGateway();
console.log('Find gateway:' + JSON.stringify(gateway));
if (req.params.pre) {
resMsg += '<b>Find gateway</b><br><pre>' + JSON.stringify(gateway, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
} else {
resMsg += '"gateway":' + JSON.stringify(gateway);
}
// client作成
var clientAll = new Client(gateway.addresses[0]);
// SECURITY_CODEでトークン取得
const {identity, psk} = await clientAll.authenticate(SECURITY_CODE);
// 接続
await clientAll.connect(identity, psk);
// デバイス参照
await clientAll.on("device updated", async (device) => {
// 情報取得
if (req.params.pre) {
resMsg += '<b>Find Device</b><br><pre>' + JSON.stringify(device, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
} else {
resMsg += ',"Device":' + JSON.stringify(device);
}
setTimeout(() => clientAll.destroy(), 500);
}).observeDevices();
resMsg += '<pre>' + JSON.stringify(result, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
}
return resMsg;
}「dimmer」にアクセスすることで、明るさが変更できる。
http://raspberrypi.local:3000/trd/andon/dimmer/100
色温度変更
明るさ同様に、色(色温度)も変更可能なようなので、実行してみる。 これも「operateLight」メソッドで、実行可能。色温度を変更する関数を用意。
async function lightTemperature(deviceName, setTemperture) {
// 色変更
// gateway検索
const gateway = await tradfri.discoverGateway();
// client作成
var client = new Client(gateway.addresses[0]);
// SECURITY_CODEでトークン取得
const {identity, psk} = await client.authenticate(SECURITY_CODE);
// 接続
await client.connect(identity, psk);
// デバイス参照
client.on("device updated", async (device) => {
if (device.name === deviceName && device.type === tradfri.AccessoryTypes.lightbulb) {
// 対象デバイスの場合、ライトを制御
await client.operateLight(device, {colorTemperature: setTemperture, transitionTime: 3});
// 後片付け処理
setTimeout(() => client.destroy(), 500);
}
}).observeDevices();
}こちらも外部呼出しで実行されるように、「lightGetAccess」関数に組み込む。 「color」とコマンド(cold/normal/warm)の指定で、色温度を変化させるように指示可能。
/* GET access. */
async function lightGetAccess(req, deviceName) {
// コマンドに合わせてLight処理
let resMsg = '';
if (deviceName) {
resMsg = 'tradfri api access[' + deviceName + ']';
if (req.params.mode.toLowerCase() === 'on') {
// 点灯
lightTo(deviceName, true);
resMsg += '<br>Light On';
} else if (req.params.mode.toLowerCase() === 'off') {
// 消灯
lightTo(deviceName, false);
resMsg += '<br>Light Off';
} else if (req.params.mode.toLowerCase() === 'dimmer') {
// 明るさ変更
let bright = 100;
if (req.params.tm) {
if (isFinite(req.params.tm)) {
bright = Number(req.params.tm)
if (bright > 100) bright = 100
}
}
// 明るさセット
lightDimmer(deviceName, bright);
resMsg += '<br>Light Dimmer ' + bright.toString();
} else if (req.params.mode.toLowerCase() === 'color') {
// 色変更
let bulbTemperature = 58.8;
if (req.params.tm) {
if (req.params.tm.toLowerCase() === 'cold') {
bulbTemperature = 0;
} else if (req.params.tm.toLowerCase() === 'normal') {
bulbTemperature = 58.8;
} else if (req.params.tm.toLowerCase() === 'warm') {
bulbTemperature = 100;
}
}
// 色セット
lightTemperature(deviceName, bulbTemperature);
resMsg += '<br>Light ColorTemperature ' + req.params.tm.toLowerCase() + ':' + bulbTemperature.toString();
} else {
var ret = await lightGet(deviceName);
// 状態返却
if (req.params.mode.toLowerCase() === 'pre') {
resMsg += '<pre>' + JSON.stringify(ret, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
} else {
resMsg += '"Device":' + JSON.stringify(ret);
}
}
} else {
// デバイス指定無しの場合は全情報返却
var result = '';
// gateway検索
const gateway = await tradfri.discoverGateway();
console.log('Find gateway:' + JSON.stringify(gateway));
if (req.params.pre) {
resMsg += '<b>Find gateway</b><br><pre>' + JSON.stringify(gateway, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
} else {
resMsg += '"gateway":' + JSON.stringify(gateway);
}
// client作成
var clientAll = new Client(gateway.addresses[0]);
// SECURITY_CODEでトークン取得
const {identity, psk} = await clientAll.authenticate(SECURITY_CODE);
// 接続
await clientAll.connect(identity, psk);
// デバイス参照
await clientAll.on("device updated", async (device) => {
// 情報取得
if (req.params.pre) {
resMsg += '<b>Find Device</b><br><pre>' + JSON.stringify(device, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
} else {
resMsg += ',"Device":' + JSON.stringify(device);
}
setTimeout(() => clientAll.destroy(), 500);
}).observeDevices();
resMsg += '<pre>' + JSON.stringify(result, undefined, 1).replace(/\n/g, '<br>') + '</pre>';
}
return resMsg;
}「color」にアクセスすることで、明るさが変更できる。
http://raspberrypi.local:3000/trd/andon/color/cold
