対話式コマンドラインで打ち込んだ言葉をGoogleHomeに話させる

我が家にGoogle Home miniが届いたので、早速遊んでいます。
といっても普通に使うだけだと、こちらから話しかけたことに対してGoogleHomeが答えてくれるだけで、正直あまり楽しくありません。毎回「ねぇGoogle」って言わなきゃいけないのも面倒だし。
どちらかと言えばGoogleHome側から「おはよう」とか「今日は○○したほうがいいよ」とか話しかけてもらいたいものです。

ということで、GoogleHomeに喋ってほしい言葉を通知する方法を調べてみました。
せっかくなのでコマンドラインからメッセージをポンポン送って、それをGoogleHomeがポンポン拾ってくれる方が応用が効きそうです。
我が家にはまだ喋れないですが娘がいるので、将来的にはGoogleHomeを人形に仕込んでおいて、会話していると見せかけて父親が後ろで会話を打ち込んでいるとか・・・、やってみたいですね。

前置きが長くなりましたが、早速実装してみます。あ、使う言語はNode.jsです。

# 開発の準備
## readline-syncのインストール
コマンドラインから対話式でメッセージを送るには、`readline-sync`を使用します。
ユーザからの入力を待ちたいところで、`readlineSync.question()`を呼び出すだけです。

```bash
# readline-syncのインストール
npm install readline-sync
```

```js:index.js
const readlineSync = require('readline-sync');
console.log('あなたの名前は？');
// 入力を待機
const input = readlineSync.question();
console.log(`こんにちは、${input}`);
```

```bash
$ node index
あなたの名前は？
蒲焼さん太郎
こんにちは、蒲焼さん太郎
```

## google-home-notifierのインストール
`google-home-notifier'`を使うと、GoogleHomeにメッセージを送ることが出来ます。
メッセージが送られると、GoogleHomeはそのメッセージをスピーカーから発します。

```bash
# google-home-notifierのインストール
npm install google-home-notifier
```

```js:index.js
const googlehome = require('google-home-notifier');
// 通知先のGoogleHomeのデバイス名を指定する
googlehome.device('GoogleHome', 'ja');
// GoogleHomeに通知する
googlehome.notify('ビクビク', function(res) {
    console.log('通知完了');
});
```

# 実装時のハマりどころ
`readline-sync`と`google-home-notifier`を組み合わせれば簡単に実装できそうですが、
実際やってみるといくつかハマったところがあるのでメモしておきます。

### google-home-norifierのインストールがやたら長い
インストール中のログを見ると無限ループしているように見えますが、焦って中断すると実行時にエラーになります。
私はこんなエラーが出たので一度アンインストールしてからインストールし直しました。

```bash
Error: Cannot find module '../build/Release/dns_sd_bindings'
at Function.Module._resolveFilename (module.js:325:15)
at Function.Module._load (module.js:276:25)
at Module.require (module.js:353:17)
at require (internal/module.js:12:17)
at Object.  (略)
```

### `googlehome.device(deviceName, lang)`に何を指定すればいいのか？
引数が`deviceName`となっているので、GoogleHomeアプリで見たときのデバイス名（「ファミリールーム」とか）を指定すればいいように思えますが、違うようです。デバイスごとに持っている一意なIDみたいなものを指定します。
IDが何なのかは、`googlehome.notify(...)`を呼ぶとコンソールに表示されます。(他に確認する方法があれば教えてください)

```bash
Device "Google-Home-45cab22cd0a389db9d852b6d9befc34a" at 192.168.11.9:8009
```

この「" "」で囲われた部分を、`googlehome.device(deviceName, lang)`の`deviceName`として指定します。
正確には、この文字列に部分一致すればいいので`Google-Home`と指定すれば大丈夫です。
もっと大胆に言えば、空文字を指定すれば必ず部分一致するので空文字指定でもいいです。
※ `null`や`undefined`はだめ
※　GoogleHomeが複数見つかった場合、最初に部分一致したものに対して通知します。

### IPアドレスでGoogleHomeを指定した場合、言語設定が効かない
[google-home-notifierの公式](https://github.com/noelportugal/google-home-notifier)では、こういう書き方が紹介されています。

```js:Usage
var googlehome = require('google-home-notifier');
var language = 'pl'; // if not set 'us' language will be used

googlehome.device('Google Home', language); // Change to your Google Home name
// or if you know your Google Home IP
// googlehome.ip('192.168.1.20', language);

googlehome.notify('Hey Foo', function(res) {
  console.log(res);
});
```

「IPアドレスが分かってるならこういう書き方が出来るよ」と`googlehome.ip(IPアドレス, language)`という書き方がコメントされていますが、
この`ip`メソッド、実装を見ると第二引数がないので言語設定が効きません。
今後修正されるかもしれませんが、今はデバイス名(ID)で指定したほうがいいです。
ちなみに言語設定が効かないと、GoogleHomeは何も喋りません。

### 連続でnotifyするとエラーになる
`notify`関数は、GoogleHomeにメッセージを通知しますが初回時は接続も行います。
接続が終わるまでに次の`notify`関数を呼び出すとエラーになります。

```js
googlehome.notify('ハロー', function(res) {
  console.log(res);
});
googlehome.notify('グッバイ', function(res) {
  console.log(res);
});
```

じゃあコールバック内で`notify`すればいいのかと言えば、それもうまく行きません。
以下のコードを動かすと、GoogleHomeは「グッバイ」とだけ話します。

```js
googlehome.notify('ハロー', function(res) {
   googlehome.notify('グッバイ', function(res) {
      console.log(res);
   });
});
```

`notify`のコールバックは、GoogleHomeに通知を送った時点で実行されるらしく、
それを受けてGoogleHomeが話し出すタイミングはさらにその後です。
そして連続で通知を受けた場合、最後に受け取ったメッセージだけを話すようです。

### readline-syncの待ち中は、GoogleHomeにメッセージが通知されない
連続した対話入力を行うには、ループ内で入力を待機する必要があります。
以下のコードは、入力を受けたらそれをGoogleHomeに通知して、すぐ次の入力を待機することを意図したものですが、これはうまく動きません。

```js
while(true) {
    // 入力を待機
    console.log('あなたの名前は？');
    const input = readlineSync.question();
    // 通知する
    googlehome.notify(input, function(res) {

    });
}
```

`readlineSync.question()`は入力待ちの間、node.jsのスレッドを停止させます。
非同期で動く`googlehome.notify()`が通知を送ろうとする前にループにより次の入力待機が始まり、
いつまでたってもGoogleHomeに通知が送られないのです。

# 実装コード (最終型)
上記のハマりどころを乗り越えて実装されたコードがこちらです。

```js:index.js
const readlineSync = require('readline-sync');
const googlehome = require('google-home-notifier');

// デバイス名
const deviceName = process.argv[2] || '';
// 言語
const lang = process.argv[3] || 'ja';

// GoogleHomeのデバイス名を指定する
googlehome.device(deviceName, lang);

// コンソールに入力した文字をGoogleHomeに通知する関数
const notifyToGoogleHome = () => {
    return new Promise((resolve) => {
        console.log('何を話す？');
        // 入力を待機
        let input = readlineSync.question();
        if (input.length > 0) {
            // 入力された文字をGoogleHomeに通知する
            googlehome.notify(input, function(res) {
                // 送信後、1秒待ってからコールバックする
                setTimeout(() =>  resolve(), 1000);
            });
        } else {
            // 入力文字がなければすぐにコールバック
            resolve();
        }
        
    });
}
(async () => {
    while (true) {
        // ループして入力を待機する
        await notifyToGoogleHome();
    }
})();
```

ポイントは非同期で動く`notifyToGoogleHome`関数で、
`googlehome.notify()`を呼び出した後１秒後にコールバックを返しています。
ループ部分では、`async`、`await`構文を使って、コールバックが帰ってきたら次のループに行くようにしています。

動かしてみるとこんな感じ。送るたびに「ビョイン…」という音がしてしまうのが残念なところですが
おおむねやりたいことはできました。

```bash
$ node index Google-Home ja
何を話す？
おはよう
何を話す？
おちんぽ
```

以上です。