Google Home/Nestを喋らせるgoogle-home-notifierの導入マニュアル [2020年12月版]

Google Home/Nestを喋らせるgoogle-home-notifierの導入マニュアル [2020年12月版]
Page content

デバイスを喋らせるおもしろさ

今は「Google Nest」へシリーズ名を変えているGoogle製のホームデバイス「Google Home」を、コマンドで好きに喋らせる仕掛けの一つとして、Node.jsで動作する次の google-home-notifier がある。

ホームデバイスを喋らせる仕掛けをセットアップしておくと、部屋にいる家族に声で伝えたいことを外出中に喋らせたり、自宅サーバの起動完了を声で知ることができたりと、様々なことに応用できて便利だ。しかしながら、google-home-notifier本家の開発当時から数年が経過している関係で、現時点でこれを導入して動かすためには追加の手順が必要となっている。

そこで、今後の自分の為も含め、2020年12月時点でのgoogle-home-notifierの導入手順をまとめる。

ブログに残したい出来事

2020年12月の初め。google-home-notifierが利用するモジュールのひとつ、次のgoogle-tts-apiが、Error: get token key failed from googleというエラーでコケるようになり、その結果google-home-notifierも使えなくなった。

自力ではエラーを解決できず苦肉の策を考えていたところ、「とりあえず暫定対応してみました」と、いきなり、パッチ主の @freddiefujiwara さんからTwitterでメンションが。こんなありがたい、嬉しい出来事はブログ記事にして残すしかない。(感謝)

google-home-notifier 導入マニュアル

導入環境

google-home-notifierの導入を試したPC環境は次の通り。

PCIPアドレスアーキテクチャOS
自宅サーバ1 (Raspberry Pi 4)192.168.1.2aarch64Ubuntu 20.04.1 LTS
自宅サーバ2 (ECS LIVA)-x86_64Ubuntu 20.04.1 LTS

Googleホームデバイスとして、サーバと同じLANに、Google-Home-Miniを2台、Chromecast-Audioを1台、無線接続している。各デバイスのIPアドレスはDHCPで割り当てている。以下の導入手順は、自宅サーバ1を対象にした例である。

導入手順

導入手順は、基本的には、google-home-notifier本家ページのListenerに記述されている内容で進める。ただし、サービスを外部公開するためのngrokの代わりに私は別の方法を使うことにして、ngrok関連の処理を外している。

  1. $ git clone https://github.com/noelportugal/google-home-notifier
  2. $ cd google-home-notifier
  3. package.jsonの「“google-tts-api”」行をこのように変更する
  4. (対象環境がラズパイの場合) 追加作業を行う→https://github.com/noelportugal/google-home-notifier#raspberry-pi
  5. $ npm install
  6. browser.jsの一部をこのように変更する
  7. 付属のexample.jsの代わりに、自作server.jsを保存する
  8. $ node server.js
  9. サーバ (192.168.1.2) の次のURLにアクセスする→http://192.168.1.2:8091/google-home-notifier?text=Hello+Google+Home

以上の、手順1~9を行うと、Google-Home-Miniが「Hello Google Home」と喋ってくれるはず。

手順3) package.jsonの変更箇所

google-tts-apiのバージョン指定を「0.0.6」に変える。google-tts-api本家ページのCHANGELOGFix the change of Google Translate APIと記述されている版だ。

$ diff -up package.json.original package.json
--- package.json.original	2020-12-31 17:50:35.286898269 +0900
+++ package.json	2020-12-31 17:51:16.306740064 +0900
@@ -17,7 +17,7 @@
     "body-parser": "^1.15.2",
     "castv2-client": "^1.1.2",
     "express": "^4.14.0",
-    "google-tts-api": "0.0.2",
+    "google-tts-api": "0.0.6",
     "mdns": "^2.3.3",
     "ngrok": "^2.2.4"
   }

手順6) browser.jsの変更箇所

google-home-notifier本家ページのAfter “npm install”の記述内容に従う。

$ diff -up browser.js.original browser.js
--- browser.js.original	1985-10-26 17:15:00.000000000 +0900
+++ browser.js	2020-12-31 17:56:17.874159294 +0900
@@ -118,7 +118,7 @@ Browser.create = function create(service
 
 Browser.defaultResolverSequence = [
   rst.DNSServiceResolve()
-, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo()
+, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({families:[4]})
 , rst.makeAddressesUnique()
 ];
 

手順7) 自作server.jsの内容

example.jsを元に、次の変更を行っている。

  • ngrok関連の処理をコメントアウト
  • 喋らせたいホームデバイスの名前 (の前方の共通部分) をdeviceNameで指定し、IPアドレスでの指定は行わないので関連の処理をコメントアウト
$ cat server.js 
var express = require('express');
var googlehome = require('./google-home-notifier');
// var ngrok = require('ngrok');
var bodyParser = require('body-parser');
var app = express();
const serverPort = 8091; // default port

// var deviceName = 'Google Home';
var deviceName = 'Google-Home-Mini';
// var ip = '192.168.1.20'; // default IP

var urlencodedParser = bodyParser.urlencoded({ extended: false });

app.post('/google-home-notifier', urlencodedParser, function (req, res) {
  
  if (!req.body) return res.sendStatus(400)
  console.log(req.body);
  
  var text = req.body.text;
  
  if (req.query.ip) {
     ip = req.query.ip;
  }

  var language = 'ja'; // default language code
  if (req.query.language) {
    language;
  }

  // googlehome.ip(ip, language);
  googlehome.device(deviceName,language);

  if (text){
    try {
      if (text.startsWith('http')){
        var mp3_url = text;
        googlehome.play(mp3_url, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will play sound from url: ' + mp3_url + '\n');
        });
      } else {
        googlehome.notify(text, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will say: ' + text + '\n');
        });
      }
    } catch(err) {
      console.log(err);
      res.sendStatus(500);
      res.send(err);
    }
  }else{
    res.send('Please GET "text=Hello Google Home"');
  }
})

app.get('/google-home-notifier', function (req, res) {

  console.log(req.query);

  var text = req.query.text;

  if (req.query.ip) {
     ip = req.query.ip;
  }

  var language = 'ja'; // default language code
  if (req.query.language) {
    language;
  }

  // googlehome.ip(ip, language);
  googlehome.device(deviceName,language);

  if (text) {
    try {
      if (text.startsWith('http')){
        var mp3_url = text;
        googlehome.play(mp3_url, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will play sound from url: ' + mp3_url + '\n');
        });
      } else {
        googlehome.notify(text, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will say: ' + text + '\n');
        });
      }
    } catch(err) {
      console.log(err);
      res.sendStatus(500);
      res.send(err);
    }
  }else{
    res.send('Please GET "text=Hello+Google+Home"');
  }
})

app.listen(serverPort, function () {
  //ngrok.connect(serverPort, function (err, url) {
  //console.log('Endpoints:');
  //console.log('    http://' + ip + ':' + serverPort + '/google-home-notifier');
  //console.log('    ' + url + '/google-home-notifier');
  //console.log('GET example:');
  //console.log('curl -X GET ' + url + '/google-home-notifier?text=Hello+Google+Home');
  //console.log('POST example:');
  //console.log('curl -X POST -d "text=Hello Google Home" ' + url + '/google-home-notifier');
  // );
})

自作server.jsのエラー問題が未解決

実は、上記の手順を経て起動した自作server.jsのサービスは、Google Homeを1度喋らせたあとにプロセスが意図せぬエラーで終了してしまう。その時の内容は次の通り。

server.js: エラー終了時のログ

$ node server.js
*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
*** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
{ text: 'Hello Google Home' }
Device "Google-Cast-Group-e8c73b83ff0b441a884936424a32536e" at 192.168.1.199:32161
Device "Google-Home-Mini-01a09e213c39b4d7151557adf3e31cf7" at 192.168.1.198:8009
timeout parameter is deprecated
Device "Google-Home-Mini-e7dac5c24c8c4720c163b58859be6799" at 192.168.1.199:8009
timeout parameter is deprecated
Device "Chromecast-Audio-42b0162ed37bd719776536eb4f73c9d5" at 192.168.1.197:8009
Device notified
Device notified
_http_outgoing.js:470
    throw new ERR_HTTP_HEADERS_SENT('set');
    ^

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (_http_outgoing.js:470:11)
    at ServerResponse.header (/home/foobar/tmp/google-home-notifier/node_modules/express/lib/response.js:771:10)
    at ServerResponse.contentType (/home/foobar/tmp/google-home-notifier/node_modules/express/lib/response.js:599:15)
    at ServerResponse.send (/home/foobar/tmp/google-home-notifier/node_modules/express/lib/response.js:145:14)
    at /home/foobar/tmp/google-home-notifier/server.js:86:15
    at /home/foobar/tmp/google-home-notifier/google-home-notifier.js:35:11
    at /home/foobar/tmp/google-home-notifier/google-home-notifier.js:70:7
    at /home/foobar/tmp/google-home-notifier/google-home-notifier.js:95:9
    at /home/foobar/tmp/google-home-notifier/node_modules/castv2-client/lib/controllers/media.js:81:5
    at fn.onmessage (/home/foobar/tmp/google-home-notifier/node_modules/castv2-client/lib/controllers/request-response.js:27:7)

server.js: googlehomenotifier.serviceでサービス化

上記のエラーは、HTTPヘッダに関する何らかの不具合が起こっているためだろうと思うが、私がNode.jsを理解できておらず、どうやってエラーを解決すればいいか分からない。うーん。

しかし、落ちてしまうサービスを起動し直せば、google-home-notifierが再び利用できることは分かっているので、ものすごいバットノウハウなのであるが、文字通り「落ちたサービスを起動し直す」ようにした。

具体的には、次のサービスファイルgooglehomenotifier.serviceを用意した上で、自作server.jsをsystemd配下のサービスとした。サービスファイルにRestartSec=5と記述しているので、server.jsのサービスが停止して5秒するとサービスを再起動してくれる。

$ cat /etc/systemd/system/googlehomenotifier.service
Description=google-home-notifier Server
After=syslog.target network-online.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/node server.js
Restart=on-failure
RestartSec=5
KillMode=process
WorkingDirectory=/home/foobar/tmp/google-home-notifier

[Install]
WantedBy=multi-user.target

P.S. ngrokの代替手段

自宅のgoogle-home-notifierをインターネットから使う、ngrok以外の手段として。

インターネットに公開されているサーバを1台用意して (以下VPSと表記)、次のような仕掛けを作ると、自宅サーバへインターネットから直接アクセスできない状態でも、https://<VPS>/google-home-notifierへPOSTした内容を自宅のGoogle Homeが喋るようにできる。

  1. VPSの8091/tcp (localhost:8091) へ通信すると、その通信がgoogle-home-notifierを動かしている自宅サーバの8091/tcpへ届くように、自宅サーバにてautosshを用いた持続的なSSHポートフォワーディングを実行する (autosshをサービス化してSSH接続を強化 [2019-07-18] の応用)
  2. VPS上のnginxでリバースプロキシを設定し、localhost:8091をHTTPS (443/tcp) で公開する

そしてさらに、上記の公開URLhttps://<VPS>/google-home-notifierへテキストをPOSTする入力フォームも用意すると、どこからでも、フォームへ入力した言葉を自宅のGoogle Homeが喋るというシステムができあがる。