Google Home、IFTTT、Firebase、Node.js、irMagicianを使ってシーリングライトを音声操作する


# はじめに
Google Homeを買ったので実用すべく、赤外線リモコン機器を音声操作出来るようにするため[irMagician](http://www.omiya-giken.com/?page_id=837)を買いました。
irMagicianを使ってまずはシーリングライトを音声操作してみようと思います。
なお前回記事ではラズパイとグローバルとの通信にngrokを使用していましたが、今回はFirebaseを使って通信を試みています。

前回記事：[Google Home、IFTTT、Googleスプレッドシートを使って独自音声コマンドでログをとる（ついでにNode.jsやngrokやらも使ってLINEやGoogle Homeに通知する）](https://qiita.com/eqbbpa/items/be562d8a823ad2639d94)

※家電の赤外線操作にはirMagicianよりも「[Google Homeに話しかけてエアコンを操作してみる](https://qiita.com/miso_develop/items/204b2e16b1e58e52dc07)」にある「RM mini3」をオススメします。。。

# 処理の流れ
![pict.png](https://qiita-image-store.s3.amazonaws.com/0/207109/3666ca63-f3fb-0ec4-d6bb-868a3f241d91.png)

# irMagicianの導入
irMagicianについて簡単に説明しますと、これはPCとUSB接続できる赤外線センサです。
WindowsやMacでも使えてドライバも不要です。
そしてPythonやRuby、Go、PHP、Node.jsと様々な言語をサポートしています。
[各種言語へのポート](http://www.omiya-giken.com/?page_id=2079)

なお温度センサつきのirMagician-Tもあり、私は今回これを購入しました。
言語はNode.jsで実装を行います。

irMagicianの使い方、Node.jsモジュールのインストールについては以下の記事を参照下さい。
[Nodeモジュール「irmagician」を作りました。 ](https://qiita.com/tamaki/items/f4127249ec211f7d70cd)

上記の記事ではOSはMacを使用されています。
ラズパイの場合との差分を以下に挙げておきます。

### デバイス確認方法
以下のコマンドで行いました。
また結果として「/dev/ttyACM0」が返ってきました。

```
$ ls /dev/ttyA*
```

### 「screen /dev/ttyACM0 9600」コマンド実行時
「... Ready」って出るらしいですが何も出ませんでした。

### irmagician（Nodeモジュール）インストール
私はpiユーザで実行していたので頭にsudoをつける必要がありました。
また私の環境ではnpmアプデしろとエラーが出たので`npm install -g npm`を実行してからirMagicianのNode.jsモジュールのインストールを行いました。

### Node.jsモジュールインストール後に「npm rebuild serialport --build-from-source」を実行
ラズパイ環境だとここが**超重要**です。
必ず以下のコマンドを実行して下さい。

```
$ su
$ cd /usr/local/lib
$ npm rebuild serialport --build-from-source
```

### writeFragment関数の修正
ここも私的には**超重要**ですが、もしかしたら私の環境だけの事象かもしれません。
jsonに保存した信号情報をロードするためのwrite関数を実行すると以下のエラーが出ます。

```
Error: Error: Bad file descriptor, calling write
```

ソース追ってデバッグしてった結果、一部の処理にwaitをかけることで改善されました。

```js:node_modules/irmagician/lib/irMagician.js
...

function writeFragment(pos, value) {
    return new Promise((resolve, reject) => {
        // console.log(`write fragment pos: ${pos} value:${value}`)
        command_immediate(`w,${pos},${parseInt(value)}\r\n`, () => {
            setTimeout(() => {	//コメントアウト外し
                resolve()
            }, 0)				//50→0に
        }, error => {
            console.log(error)
            reject(error)
        })
    })
}

...
```

上記のコメントを入れた行がデフォルトではコメントアウトされているのでコメントアウトアウトして下さい。
それとsetTimeoutの時間が50msecとなっていますが、これだと遅かったので思い切って0にしました。

### captureについて
それとラズパイ関係ないのですが私のような初心者向けの補足として、captureの最後に出てくる数字が赤外線信号のバイト数になります。
irMagicianのメモリは640バイトなので、ここの数字が640を超えたらそのリモコンはirMagicianでは使用不可（だと思います…）。

### temp関数の実装
irMagician-Tの温度センサを使用するためには「t」コマンドに対応した関数を実装する必要があります。
以下の記事を参考に実装しました。
[二足歩行ロボット Rapiro を Node.js で制御 [7] 家電の操作](https://qiita.com/mkoku/items/3c211f3e7e5deddcdd7b#_reference-dcbb7f24c3ca96b0629b)

### postScaler関数の実装
赤外線信号のキャプチャがうまく出来ない場合の悪あがきとして「k」コマンドにてpost scalerを変更する方法があるようです。
Node.jsモジュールでもこのコマンドが実行出来るよう個人的にカスタマイズしました。
irMagician.jsの方はファイル末尾にでも、cli.jsは最後のswitch文のどこかに追記しておいて下さい。

```js:node_modules/irmagician/lib/irMagician.js
...

function postScaler(value, port) {
    return new Promise((resolve, reject) => {
        // if (this.port.isOpen() == false) return
        if (value < 1 || value > 255) reject("value over!")
        var commandStr = "k," + value + "\r\n"
        this.port.write(commandStr, error => {
            if (error) {
                console.log("Error: ", error.message)
                reject(error)
            }
            this.port.on("data", (data) => {
                // console.log(data.toString())
                resolve()
            })
        })
    })
}

exports.postScaler = (value, port) => {
    co(function* () {
        var validPort = yield autoSelectDevicePort()
        port = port || validPort
        yield open(port)
        yield postScaler(value)
        yield close()
    }).catch(error => {
        console.log(`💀  Error: ${error}`)
        close()
    })
}
```

```js:node_modules/irmagician/bin/cli.js
...
case "postScaler":
    if (cli.input[1]) {
        irMagician.postScaler(cli.input[1])
    } else {
        cli.showHelp()
    }
    break
default:
    cli.showHelp()
    break
}
...
```

ラズパイ環境では以上の事に注意すればとりあえずは大丈夫だと思います。

# Firebaseの設定
続いてFirebaseの設定を行っていきましょう。
設定にあたり以下の記事を参考にしました。
[新着メールを知らせてくれるGoogle Home](https://qiita.com/tetotetote/items/4d6ac866b716695fa4c9#_reference-e0362f4ee834c2c58f28)

最近ngrokを使用しないグローバル - ローカル間の通信としてFirebaseを使用しようとFunctionsへのWEB API実装等を調べていましたが、上記の記事ではRealtime Databaseの更新イベントをフックするという方法を用いており勉強になりました。

以下の項目を参照しFirebaseプロジェクト、Databaseの作成を行います。
[Firebaseの設定](https://qiita.com/tetotetote/items/4d6ac866b716695fa4c9#firebase%E3%81%AE%E8%A8%AD%E5%AE%9A)

なお私はDatabaseを以下のように作成しました。

```
プロジェクト名-xxxxx
 └ googlehome
 　 └ word: ""
```

↑のDatabaseの場合「`https://プロジェクト名-xxxxx.firebaseio.com/googlehome/word.json`」へBodyに何か値を入れてPUTアクセスするとwordの値が更新されます。
とても便利ですね。

# IFTTT Applet作成
上記の記事ではGmailをトリガーにWebhooksでFirebaseのDatabase更新を行っていますが、今回はGoogle Homeへの音声コマンドがトリガーとなるため、「this」にはGoogle Assistantを使います。

## 「this」のGoogle Assistantレシピ
トリガー部には音声コマンド内に変数が使用できる「Say a phrase with a text ingredient」を使用します。
「電気」「ライト」どちらの単語からも起動するようにし、後に続く言葉（「つけて」「消して」等）は変数（$）に持ちます。
![Make an Applet - IFTTT_20171022_032022.png](https://qiita-image-store.s3.amazonaws.com/0/207109/8df3db44-f3d4-00cf-6cb9-51fe42fa2403.png)

## 「that」のWebhooksレシピ
アクション部にはFirebaseのDatabase更新のためWebhooksでHTTPアクセスを行います。
URLは先程作成したDatabaseの構造に沿い、MethodはPUT、Content Typeはjson、そしてBodyに"light {{TextField}}"と設定します。
「{{TextField}}」にthisの「$」部の変数（「つけて」「消して」等）が入ってきます。
注意点はBodyを必ずダブルクォーテーションで括るのと、「light」と「{{TextField}}」の間にスペースを入れることです。
![Make an Applet - IFTTT_20171022_032135.png](https://qiita-image-store.s3.amazonaws.com/0/207109/e2bed9f2-b316-3004-318d-a40bfc46ec81.png)

以上でIFTTTの設定は完了です。
この時点でGoogle Homeに喋りかけるだけでFirebaseのDatabaseが更新されるようになっています。

# Node.jsの実装
さていよいよ大詰めです。
Node.jsモジュールのインストールとindex.jsの実装を行います。

## Node.jsモジュールのインストール
FirebaseとSuperAgentのNode.jsモジュールをインストールします。
例としてホームディレクトリに「firebase」というディレクトリを作成しインストールを行ってみましょう。
以下のコマンドを実行してみて下さい。

```
$ mkdir firebase
$ cd firebase
$ npm install firebase
```

## index.jsの実装
以下のように実装を行います。
confingはFirebaseコンソールの「ウェブアプリに Firebase を追加」より取得できる情報に置き換えて下さい。

```js:index.js
var firebase = require("firebase");

//firebase config
var config = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxx-xxxxx.firebaseapp.com",
  databaseURL: "https://xxxxxxxx-xxxxx.firebaseio.com",
  projectId: "xxxxxxxx-xxxxx",
  storageBucket: "xxxxxxxx-xxxxx.appspot.com",
  messagingSenderId: "xxxxxxxxxxxxxxxx"
};
firebase.initializeApp(config);

//jsonからvalueに一致する値取得
const getJsonData = (value, json) => {
  for (var word in json)  if (value == word) return json[word]
  return json["default"]
}

//database更新時
const path = "/googlehome";
const key = "word";
const db = firebase.database();
db.ref(path).on("value", function(changedSnapshot) {
  //値取得
  const value = changedSnapshot.child(key).val();
  if (value) {
  	console.log(value);
  	
    //コマンド生成
    const command = getJsonData(value.split(" ")[0], {
      
      //シーリングライト
      "light": () => {
        const command = "sh /home/pi/irmagician/light/ir.sh ";
        const option = getJsonData(value.split(" ")[1], {
          "つけ": "full",
          "オン": "full",
          "消し": "off",
          "けし": "off",
          "オフ": "off",
          "エコ": "eco",
          "楽": "easy",
          "ラクニエ": "easy",
          "節電": "save",
          "おやすみ": "sleep",
          "タイマー": "sleep",
          "保安": "security",
          "豆": "security",
          "a": "a",
          "1": "a",
          "b": "b",
          "2": "b",
          "に": "b",
          "c": "c",
          "3": "c",
          "さん": "c",
          "傘": "c",
          "館": "c",
          "d": "d",
          "4": "d",
          "音": "d",
          "屋": "d",
          "default": false
        });
        return option ? command + option : option;
      },
      
      //template
      "xxx": () => {
        return getJsonData(value.split(" ")[1], {
          "xxx": "xxx",
          "default": false
        });
      },
      
      //default
      "default": () => false,
      
    })();
    console.log(command);
    
    //コマンド実行
    if (command) {
      const exec = require('child_process').exec;
      exec(command);
      
      //firebase clear
      db.ref(path).set({[key]: ""});
    }
    
  }
});
```

先程IFTTTで起動ワードを「電気」「ライト」だけにしておいたのはこちらの実装でその先の動作を定義するためです。
IFTTTだとGUI操作での設定になり作成や管理が面倒なのでコード側で管理することにしました。
定義を行っているjsonのキー側がGoogle Assistantにて認識されるワード、値側が実行されるコマンドのオプションになります。

「つけて」や「消して」が先頭2文字だけになっているのはGoogle Assistantで認識された際に「つけ て」や「消し て」とスペースが差し込まれ言葉が分割されてしまうためです。
あくまでGoogle Assistantに認識される形式で言葉を定義します。

そして実行されるコマンドのshは以下になります。

```bash:ir.sh
#!/bin/sh
cd `dirname $0`
echo ir light $1!

#json有無チェック
if test -e ./json/$1.json; then
    #キャプチャしておいた赤外線信号をロード
	irMagician write ./json/$1.json
    #赤外線信号送信
	irMagician play
else
	echo そんな設定ないよ！
fi
```

今回私は「/home/pi/irmagician」にirMagician用プロジェクトを作成しており、その配下に「light」ディレクトリを作成し「ir.sh」を格納しています。
さらのその配下として「json」ディレクトリがあり、そこにirMagicianにてキャプチャした赤外線信号データのjsonが格納されています。
このshは引数に受け取った名前のjsonデータをチェックし、あればirMagicianにロードして信号送信を行っています。

以上でNode.jsの実装は完了です。
firebaseプロジェクトディレクトリにて以下のコマンドを実行してGoogle Homeに話しかけてみましょう。

```
$ node index.js
```

どうでしょう。
ちゃんと話しかけたとおり動作しましたか？
正常に動いているなら後はサービス化なりforeverなりしちゃいましょう。

# 実際の動作
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Google Homeに話しかけて電気を操作 <a href="https://t.co/T1FyIzLeDf">https://t.co/T1FyIzLeDf</a></p>&mdash; 田中みそ (@miso_develop) <a href="https://twitter.com/miso_develop/status/921835537790414848?ref_src=twsrc%5Etfw">2017年10月21日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

# おわりに
irMagicianの導入ではwrite関数が動かなかったりそれを直したりと色々格闘しましたが、なんとかシーリングライトをGoogle Homeで操作することが出来ました。
念願のスマートホーム化に一歩近づきました。

この勢いでテレビとエアコンも音声操作しようとしたのですが、テレビはリモコンが赤外線式ではないためキャプチャ不可、エアコンは赤外線の信号量が864バイトとirMagicianのメモリオーバー（640バイトまで）でキャプチャ不可という状況に…

ただ、テレビに関してはほぼ娘の教育テレビ再生専用器であるため、録画を行っているPS4（nasne）を音声操作出来れば要件を満たせます。
で、どうにかできないか調べてみたところ[ps4-waker](https://github.com/dhleong/ps4-waker)というNode.jsモジュールがあるようで…
次はこれを使ってPS4の音声操作に挑戦してみようと思います。

**※2017/10/24追記　ps4-wakerを使ってPS4も音声操作できるようにしました。**
**[Google Homeに話しかけてPS4を操作してみる](https://qiita.com/miso_develop/items/0175c5f45a1e90464234)**