【ずぼら向けIoT】ゴミ出し曜日の朝にGoogle Homeが音声通知

夫です。

ゴミ出し曜日の朝に、Google Home miniが音声で通知してくれるようにしました。

f:id:daredemosmart:20180810221401p:plain

やろうと思った経緯

私が住んでいる地域では、ゴミの種類によってゴミを出せる曜日が決まっており、その曜日のAM 9:00までに出しておく必要があります。出し忘れることが多かったので、IFTTTを使ってゴミ出し曜日の朝にLINEから通知がくるよう設定したのですが、朝急いでいるとLINEすら見ずに家を出ることも多々あり、あまり機能していませんでした。

そこで、LINE通知ではなく、Google Home miniに「今日はゴミ出しの日です」と喋らせる方が、出し忘れ対策になると考えました。

システム構成(最終的にちょっと変わります)

システム構成図を以下に図示します。今回、Google Homeを自発的に喋らせるために、google-home-notifierというnodeモジュール を使用します。さらに、google-home-notifierのプログラムをIFTTTのwebhooksでアクセスできるよう、ngrokというサービスでプログラムを外部公開します。

f:id:daredemosmart:20180810171811p:plain

しかし、本システムを実現する上で、ngrokに関して制限事項があり、実運用で使用するにはもう一工夫が必要になります。本記事後半にて、その制限事項と解決策、最終的なシステム構成についても記載します。

システム構築手順

事前準備

Google Homeの初期セットアップを済ませておく必要があります。
 ※Google Homeの初期セットアップ手順については別途記事作成予定

・IFTTTのアカウント登録も事前に済ませておく必要があります。
 未登録の方は以下記事を参考に登録しておいて下さい。

【ずぼら向けIoT】Google HomeからメモをLine通知【IFTTT】 - 誰でもスマートホーム ~ずぼら夫婦の賃貸暮らし~

・サーバーPCを立ち上げる必要がありますので、
 PCのセットアップとNode.jsのインストールは行っておいて下さい。
 ※各OSにおけるNode.jsインストール手順については別途記事作成予定

google-home-notifier導入

※私の実行環境はUbuntu18.04ですので、環境によってはコマンドや結果に差異がある可能性があります。ご了承下さい。

まず、必要なパッケージをインストールして下さい(これらがないと、google-home-notifierインストール時にエラーが発生します)。

$ sudo apt-get install git-core libnss-mdns libavahi-compat-libdnssd-dev 

任意の場所にプロジェクト用ディレクトリを作成し移動します。このとき、プロジェクト用ディレクトリの名前が「google-home-notifier」だと、google-home-notifierモジュールのインストールに失敗しますので、異なる名前を設定して下さい。

$ mkdir dust-notifier
$ cd dust-notifier/

 以下のコマンドを実行し、プロジェクトを作成します。

$ sudo npm init -y

 以下のコマンドを実行し、google-home-notifierモジュールをインストールします。

 以下のようなワーニングが表示されましたが、無視します。

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN dust_notifier@1.0.0 No description
npm WARN dust_notifier@1.0.0 No repository field.

「(プロジェクトディレクトリ)/node_modules/mdns/lib/browser.js」を、以下の通り修正して下さい(「{families:[4]}」を追加)。  

<変更前>
Browser.defaultResolverSequence = [
  rst.DNSServiceResolve(), 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo()
, rst.makeAddressesUnique()
];

<変更後>
Browser.defaultResolverSequence = [
  rst.DNSServiceResolve(), 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({families:[4]})
, rst.makeAddressesUnique()
];

これで、導入完了です。

以下のようなテストプログラム「main.js」を 「(プロジェクトディレクトリ)/node_modules/google-home-notifier」に作成して実行すると、ネットワークに接続しているGoogle Homeが「こんにちは、私はGoogle Homeです」と喋り出します。 

var googlehome = require('./google-home-notifier');
var language = 'ja'; 

googlehome.device('Google-Home', language);
googlehome.notify('こんにちは。私はGoogle Homeです', function(res) { 
  console.log(res); 
});

実行時、以下のようなメッセージが出力されました。  

*** 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>
Device "Google-Home-Mini-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" at 192.168.0.XXX:8009
Device "Google-Home-Mini-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" at 192.168.0.XXX:8009
Device notified
Device notified
});
 

 最初の6つのワーニングは、今回参考にした他の記事などでも出力されているようなので問題なさそうです。 上記ソースコードだと、IPアドレスを指定していないため、ネットワーク内の全Google Homeバイスが喋りだします。以下の通りにIPアドレスを指定すれば、特定のGoogle Homeに対してのみ喋らせることが可能です。 

var googlehome = require('./google-home-notifier');
var language = 'ja'; 

googlehome.device('Google-Home', language); googlehome.ip('192.168.0.XXX', language); googlehome.notify('こんにちは。私はGoogle Homeです', function(res) { console.log(res); });

ngrokによる外部公開

今回は、IFTTTからGoogle Homeを喋らせるため、外部からアクセスできるようプログラムを公開する必要があります。前述した通り、これには「ngrok」というサービスを使用します。

google-home-notifier導入」の手順を実行していれば、ngrokは既にプロジェクトディレクトリにインストールされています。google-home-notifierのサンプルプログラム「(プロジェクトディレクトリ)/node_modules/google-home-notifier/example.js」は、ngrokを使用して外部公開し、アクセスURLを表示するプログラムになっています。試しに実行してみましょう。

$ node example.js & 

実行すると、以下のメッセージが表示されます(ワーニングは省略)。

POST "text=Hello Google Home" to:
    http://localhost:8080/google-home-notifier
    https://xxxxxxxx.ngrok.io/google-home-notifier
example:
curl -X POST -d "text=Hello Google Home" https://xxxxxxxx.ngrok.io/google-home-notifier

表示された「https://~」のURLが、外部公開されているアドレスになります。最後の行に表示されたコマンドを実行すると、ネットワーク内のGoogle Homeが「Hello Google Home」と喋り出すはずです。

コマンドの「Hello Google Home」の部分を他の文章に変更すれば、その文章を喋ってくれますが、サンプルプログラムのままだと日本語が喋られません。「example.js」を以下のように修正すると日本語も喋られるようになります。 

var express = require('express');
var googlehome = require('./google-home-notifier');
// ↓ 追加「日本語対応」 //
var language = 'ja';
// ↑ 追加「日本語対応」 //
var ngrok = require('ngrok');
:
中略
:
var deviceName = 'Google Home';
// ↓ 変更 「日本語対応」//
googlehome.device(deviceName, language);
// ↑ 変更 「日本語対応」//
// googlehome.accent('uk'); // uncomment for british voice
:
後略
:
  これなら、IFTTTのアプレットでWebhooksをアクションに設定し、公開URLに対して「text=今日はゴミ出しの日です」をPOSTしてやれば、サンプルプログラムだけで今回の要件は満たせるように思えます。しかし、実際は現状のプログラムだと以下2点の問題が発生します。

・作成されたURLは、8時間で失効する
・サービス起動の度に、外部公開用URLが変更される(ngrokの仕様)

今回、以下記事を参考に上記問題点を回避しました。

qiita.com

「8時間でngrokの外部公開用URLが失効する」件の解決

ngrokにアカウント登録し、認証トークンを取得することで回避できます。
以下のURLにアクセスして下さい。

ngrok.com

右上の「SIGN UP」をクリックすると、アカウント登録画面に移行しますでの、必要情報を入力して登録して下さい。GitやGoogleのアカウントでも登録可能です。

f:id:daredemosmart:20180810193127p:plain

ログイン画面に移行できれば、左メニュー欄の「Auth」を選択すると、認証トークンが表示されますので控えておいて下さい(「Copy」ボタンを押してもなぜかクリップボードにコピーされませんでした)。

f:id:daredemosmart:20180810193210p:plain

サーバPC側でngrokの設定・起動を行います。まず、以下のコマンドでngrokの実行ファイルがあるディレクトリまで移動して下さい。 

$ cd (プロジェクトディレクトリ)/node_modules/ngrok/bin/

そして、以下のコマンドで認証トークンの設定を行います(xxx~は先程控えた認証トークン)。 

$ ./ngrok authtoken xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

「(プロジェクトディレクトリ)/node_modules/google-home-notifier/example.js」を以下のように編集して実行することで、サービス終了までURLが失効しなくなります(tokenのxxx部分は、取得した認証トークンをコピペして下さい)。   

:
前略
:
var app = express();
// ↓ 変更 「8時間でngrokの外部公開用URLが失効する」//
const serverPort = 8091; 
// ↑ 変更 「8時間でngrokの外部公開用URLが失効する」//
// ↓ 追加 「8時間でngrokの外部公開用URLが失効する」//
const token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; 
// ↑ 追加 「8時間でngrokの外部公開用URLが失効する」//
:
中略
:
app.listen(serverPort, function () {
// ↓ 変更 「8時間でngrokの外部公開用URLが失効する」//
    ngrok.connect({authtoken: token, addr: serverPort}, function (err, url) {
// ↑ 変更 「8時間でngrokの外部公開用URLが失効する」//
    console.log('POST "text=Hello Google Home" to:');
:
後略
:
 

 なお、私の環境では、以下のようにURLが「undefined」になる現象が発生しましたが、ngrokやサンプルプログラムの再起動などを実行するといつのまにか解消されました。ちゃんとした原因と解決策が分かれば追記します。  

POST "text=Hello Google Home" to:
    http://localhost:8091/google-home-notifier
    undefined/google-home-notifier
example:
curl -X POST -d "text=Hello Google Home" undefined/google-home-notifier

「サービス起動の度にngrokの外部公開用URLが変更される」件の解決

上で紹介した記事と同じく、以下のアプローチをとります。

1.google-home-notifierのサンプルプログラムを、
 公開用URLをGoogleスプレッドシートに書き出すよう修正する。

2.公開用URLをスプレッドシートから読み込み、
 POST処理を行うGoogle Apps Scriptを作成する。

3.IFTTTのアクションでGoogle Apps Scriptを実行する

従って、最終的なシステム構成図は以下のようになりました。f:id:daredemosmart:20180810174450p:plain

公開用URLをGoogleスプレッドシートに書き出すよう修正

API認証キー取得

今回、Googleスプレッドシートを使用して連携を行いますが、これにはまずAPI認証キーを取得する必要があります。まずはGoogle Developer Consoleにアクセスして下さい。

console.developers.google.com

はじめてアクセスする人は、以下のような画面が表示されます。メルマガについては受信したくなければ「いいえ」、利用規約については「はい」にチェックを入れ、「同意する」をクリックして下さい。

f:id:daredemosmart:20180810180654p:plain

以下のダッシュボードが表示されます。最初はプロジェクトを作成する必要がありますので「プロジェクトを選択」をクリックして下さい。

f:id:daredemosmart:20180810180742p:plain

以下の画面が表示されますので、「作成」をクリックして下さい。

f:id:daredemosmart:20180810180806p:plain

以下の画面が表示されますので、任意のプロジェクト名を入力して「作成」をクリックして下さい。

f:id:daredemosmart:20180810180900p:plain

ダッシュボード画面に戻るので、画面上部の検索ボックスに「Google Sheets API」を入力し、Enterキーを押して下さい。すると、以下の画面が表示されますので、「有効にする」をクリックして下さい。

f:id:daredemosmart:20180810180922p:plain

最初の画面に戻り、左メニューの認証情報をクリックし、画面中央部の「認証情報を作成」で「サービスアカウントキー」を選択して下さい。

f:id:daredemosmart:20180810180936p:plain

以下の画面が表示されますので、「サービスアカウント名」に適当な名前を入力し、「役割」に「Project / オーナー」を選択し、キーのタイプが「JSON」になっていることを確認して「作成」をクリックして下さい。

f:id:daredemosmart:20180810181035p:plain

これにより、認証キーが保存できます。

Googleスプレッドシート設定

Google Driveで新規スプレッドシートを作成します。

f:id:daredemosmart:20180810183334p:plain

スプレッドシートに適当に名前を付けて右上の「SHARE」をクリックすると以下の画面が表示されます。先程取得した認証キーファイルの「client_email」を入力欄にコピペして「Send」をクリックして下さい。

f:id:daredemosmart:20180810183354p:plain

google-home-notifier サンプルプログラム修正

プロジェクトディレクトリに、スプレッドシートのモジュールをインストールして下さい。  

$ npm install npm i google-spreadsheet

 「(プロジェクトディレクトリ)/node_modules/google-home-notifier/example.js」を以下のように編集して実行して下さい。 

var express = require('express');
var googlehome = require('./google-home-notifier');
// ↓ 追加「日本語対応」 //
var language = 'ja';
// ↑ 追加「日本語対応」 //
var ngrok = require('ngrok');
var bodyParser = require('body-parser');
// ↓ 追加「サービス起動の度に、ngrokの外部公開用URLが変更される」 //
var GoogleSpreadsheet = require('google-spreadsheet');
var ngrokUrlSheet = new GoogleSpreadsheet('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); //コピーしたスプレッドシートのKey 
var credentials = require('./IoT-Smart-Home-xxxxxxxxxxxx.json'); //作成した認証キーへのパス
// ↑ 追加「サービス起動の度に、ngrokの外部公開用URLが変更される」 //
var app = express();
// ↓ 変更 「8時間でngrokの外部公開用URLが失効する」//
const serverPort = 8091; 
// ↑ 変更 「8時間でngrokの外部公開用URLが失効する」//
// ↓ 追加 「8時間でngrokの外部公開用URLが失効する」//
const token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; 
// ↑ 追加 「8時間でngrokの外部公開用URLが失効する」//
var deviceName = 'Google Home';
// ↓ 変更 「日本語対応」//
googlehome.device(deviceName, language);
// ↑ 変更 「日本語対応」//
// googlehome.accent('uk'); // uncomment for british voice

var urlencodedParser = bodyParser.urlencoded({ extended: false }); // ↓ 追加「サービス起動の度に、ngrokの外部公開用URLが変更される」 // var sheet; ngrokUrlSheet.useServiceAccountAuth(credentials, function(err){ ngrokUrlSheet.getInfo(function(err, data){ sheet = data.worksheets[0]; }); }); // ↑ 追加「サービス起動の度に、ngrokの外部公開用URLが変更される」 // : 中略 : app.listen(serverPort, function () { // ↓ 変更 「8時間でngrokの外部公開用URLが失効する」// ngrok.connect({authtoken: token, addr: serverPort}, function (err, url) { // ↑ 変更 「8時間でngrokの外部公開用URLが失効する」// console.log('POST "text=Hello Google Home" to:'); console.log(' http://localhost:' + serverPort + '/google-home-notifier'); console.log(' ' +url + '/google-home-notifier'); console.log('example:'); console.log('curl -X POST -d "text=Hello Google Home" ' + url + '/google-home-notifier'); // ↓ 追加「サービス起動の度に、ngrokの外部公開用URLが変更される」 // // sheetの一番左上のCellを取得 sheet.getCells({ 'min-row': 1, 'max-row': 1, 'min-col': 1, 'max-col': 1, 'return-empty': true }, function(error, cells) { var cell = cells[0]; cell.value = url + '/google-home-notifier'; //アクセスしてほしいURLをセット cell.save(); //保存 console.log('spread sheet update successful!!'); }); // ↑ 追加「サービス起動の度に、ngrokの外部公開用URLが変更される」 // }); })

以下が出力されれば成功です。  

POST "text=Hello Google Home" to:
    http://localhost:8091/google-home-notifier
    https://xxxxxxxx.ngrok.io/google-home-notifier
example:
curl -X POST -d "text=Hello Google Home" https://xxxxxxxx.ngrok.io/google-home-notifier
spread sheet update successful!!
 

スプレッドシートの1行1列目セルに、URLが書き込まれているはずです。

f:id:daredemosmart:20180825080959p:plain

Google Apps Scriptを作成

まずはChormeの拡張機能Google Apps Script」をインストールして下さい。

chrome.google.com

これにより、Google DriveからGoogle Apps Scriptが選択できるようになります。 

f:id:daredemosmart:20180810184940p:plain

Google Apps Scriptをクリックするとエディタ画面が表示されますので、以下のように編集して下さい(「Sheet1」はスプレッドシートのシート名に応じて変更して下さい)。    

// Postメソッドでリクエストされたときに起動する関数

function doPost(e) {
  var jsonString = e.postData.getDataAsString();
  var data = JSON.parse(jsonString);
  var options =
  {
        "method" : "post",
        "payload" : data
  };
  UrlFetchApp.fetch(getNgrokUrl(), options);
}

// スプレッドシートからngrokのURLを取得する関数
function getNgrokUrl() {
  if (getNgrokUrl.instance) { return getNgrokUrl.instance; }
  var ngrokSheetId = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; //スプレッドシートのId
  var url = SpreadsheetApp.openById(ngrokSheetId).getSheetByName("Sheet1").getDataRange().getValues()[0][0];
}

編集ができたら、「notify2GoogleHome」を選択し、画面上部メニューの「Publish / Deploy as web app」をクリックして下さい。

f:id:daredemosmart:20180810185031p:plain

以下の画面が表示されますので、適当にバージョン名を入力し、公開先を「Anyone, even anonymous」に設定して「Deploy」をクリックして下さい。

f:id:daredemosmart:20180810185052p:plain

Authorization requiredが表示された場合は、「Revier Permissions」をクリックし、自身のGoogleアカウントを選択して下さい。以下のエラーダイアログが表示される場合は、左下の「Advanced」をクリックして下さい。

f:id:daredemosmart:20180810185111p:plain

「Go to xxxxxx (unsafe)」をクリックして下さい(私の環境では、プロジェクト名は設定したにもかかわらず、なぜか「Untitled project」になっていました。)。

f:id:daredemosmart:20180810185129p:plain

以下が表示されるので、「ALLOW」をクリックして下さい。

f:id:daredemosmart:20180810220152p:plain

以下の画面が表示されれば、スクリプトの外部公開成功です。アクセス用URLが表示されているのでコピーして控えておいて下さい。

f:id:daredemosmart:20180810185209p:plain

IFTTTのアクションでGoogle Apps Scriptを実行する

Webhooksで公開したスクリプトにPOSTメッセージを送信します。今回設定するべきトリガーは「ゴミ出し曜日の朝になったら」ですが、まずは動作確認のために任意のタイミングで発動できるトリガーを設定しましょう。私は「google homeに 『ねぇ Google、ゴミ』と言ったら」をトリガーにしました。

f:id:daredemosmart:20180810185316p:plain

アクションにはWebhooksを選択し、「URL」にGASの外部公開用URL、「Content Type」に「application/json」、「Body」に「{"text":"今日はゴミ出しの日です"}」を設定して下さい。

f:id:daredemosmart:20180810185332p:plain

このアプレットを有効することで、Google Homeに「ねぇ Google、ゴミ」と話しかけるとGoogle Homeが「今日はゴミ出しの日です」と喋り出すようになりました。

あとは、同じアクション設定でトリガーを「Data & Time」にしたアプレットを作成すれば完了です。 

f:id:daredemosmart:20180810185400p:plain

残件

私の環境では、Post実行時に以下のエラーが発生しました。 

{ text: 'Hello Google Home' }
Device "Google-Home-Mini-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" at 192.168.0.XXX:8009
Device "Google-Home-Mini-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" at 192.168.0.XXX:8009
Device notified
Google Home will say: Hello Google Home
xxxx@xxxx-PC:~/dust_notifier/node_modules/google-home-notifier$ Device notified
_http_outgoing.js:471
    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:471:11)
    at ServerResponse.header (/home/xxxxxxx/dust_notifier/node_modules/express/lib/response.js:767:10)
    at ServerResponse.send (/home/xxxxxxx/dust_notifier/node_modules/express/lib/response.js:170:12)
    at /home/xxxxxxx/dust_notifier/node_modules/google-home-notifier/example.js:46:13
    at /home/xxxxxxx/dust_notifier/node_modules/google-home-notifier/google-home-notifier.js:34:11
    at /home/xxxxxxx/dust_notifier/node_modules/google-home-notifier/google-home-notifier.js:69:7
    at /home/xxxxxxx/dust_notifier/node_modules/google-home-notifier/google-home-notifier.js:94:9
    at /home/xxxxxxx/dust_notifier/node_modules/castv2-client/lib/controllers/media.js:81:5
    at fn.onmessage (/home/xxxxxxx/dust_notifier/node_modules/castv2-client/lib/controllers/request-response.js:27:7)
    at fn.emit (events.js:187:15)

現状のソースコードでは、Google Homeを喋らせたあと、send関数でPostの応答を返すのですが、Google Homeが複数台ある場合、1回のPostで複数回応答を返すことになり、エラーになるようです。

一旦、IPアドレスを指定して1台のGoogle Homeのみ喋らせるようにすることで回避できましたが、本来の用途としては全Google Homeに喋らせたいです(家のどこにいても通知を受け取りたいので)。

同じような仕組みをもう1セット実装して立ち上げれば実現できるでしょうが、もっとスマートな方法がありそう・・・ 

まとめ

google-home-notifierを使用してGoogle Homeを自発的に喋らせることができました。外部サービスとの連携も、Google Apps Sciptやスプレッドシートなど、Googleの様々なサービスを駆使することで、Webアプリケーションやスクリプトの知識が乏しい私でも実現できました。本当に便利な時代になったと思います。

なお、対応中に気づいたのですが、Google Apps Scriptは「決まった時間にscriptを実行する」などの設定が可能なので、今回の対応もIFTTTは使用せずに実現可能ですし、外部公開のリスクも若干減るのでそうするべきだと思います(この記事を作成しているときも、公開URLを掲載してしまわないよう慎重に作成しました)。さらにいうと、ゴミ出し曜日の日に「今日はゴミ出しですよ」って音声を流すタイマーをスマートフォンとかで設定しておいても事足りますね(外出中に発動してしまう可能性がありますが)。

しかし、別の用途でGoogle Homeを喋らせたい際、トリガーを他のWebサービスにする場合はIFTTTを使用する必要があるので、ノウハウを得るためにも一旦IFTTTで実現させることにしました。