32
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

IoTLTAdvent Calendar 2017

Day 17

Google Homeにセンサーの値を言ってもらう

Last updated at Posted at 2017-12-16

Google HomeはIFTTTと組み合わせると、声で指示してIFTTTと連携したサービスを起動できます。今回やりたいのは、次の会話のようにGoogle Homeに声で指示して、外部のサービスの起動し、結果をGoogle Homeに喋ってもらうことです。

私:「ねぇ、グーグル、外は何度?」
GH君:・・・ベランダの温度センサーの値を読み取って・・・
  「外の温度は10度です」

Google Home

構成

IFTTTは「もし『これ』が起きたら『あれ』をする」という処理が作れるサービスで、『これ』の部分をトリガー、『あれ』の部分をアクションと呼びます。Google HomeはIFTTTのトリガーとなる部品が提供されているので、Google Homeに声で指示すると、それがIFTTTのトリガーになり、さまざまなアクションを起動できるようになります。ところがGoogle Homeのアクション部品は提供されていないので、IFTTTだけではGoogle Homeに文章を喋らせることができません。

調べてみると、google-home-notifier.jsというソフトがあり、それを使うとGoogle Homeに指定した文章を喋らせることができるようです。

次の図のようにGoogle HomeとIFTTTを組み合わせて声で指示し、IFTTTからのメッセージをHTTPサーバーで受けてセンサーの値を取得し、google-home-notifierでGoogle Homeに喋らせるようにすれば、上記の会話を実現できそうです。

構成

google-home-notifierはNode.jsが動けばどのプラットフォームでもよさそうですが、IFTTTから起動することを考えて、Raspberry Pi(RPi)で動かすことにします。

実験1: google-home-notifierのインストールと動作確認

google-home-notifierを動かすにはNode.jsとnpmが必要なので、「Raspberry PiにNode.jsとnpmの最新版をインストールする」などを参考にしてRPiにインストールします。

次にgoogle-home-notifierをインストールします。

適当なディレクトリーで
pi$ npm init
pi$ npm install google-home-notifier

次のプログラムにGoogle HomeのIPアドレスを設定して動かすと、Google Homeが「こんにちは」と喋ります。googlehome.device()の第1引数は適当な名前をつければよいようです。

test1.js
var googlehome = require('google-home-notifier');
var language = 'ja'; // if not set 'us' language will be used

googlehome.device('リビング', language);
googlehome.ip('192.168.xx.xx');

var text = 'こんにちは';

try {
    googlehome.notify(text, function(notifyRes) {
        console.log(notifyRes);
    });
} catch(err) {
    console.log(err);
}
pi$ node test1
*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.

WARNINGがいくつかでますが、動作するので、気にしないことにします。

実験2: Ambientからセンサー値を読み、Google Homeに言ってもらう

RPiを使っているので、センサーを直接RPiにつないで、センサー値を取得することもできます。センサー値をIoTデーター可視化サービスAmbientに送っている場合はAmbientに送られたセンサーデーターを次のようにnode.jsで読み出すことができます。

node.js
var ambient = require('ambient-lib');

ambient.connect(チャネルID, 'ライトキー', 'リードキー');

ambient.read({n: 件数[, skip: スキップ件数]}, function(err, res, data) {
    ...
});

Ambientのnode.jsライブラリーの詳細は「node.jsライブラリー ambient-lib」をご覧ください。このライブラリーを使って、最初のプログラムを修正しAmbientからセンサーデーターを読み、Google Homeに言ってもらうようにします。

test2.js
var googlehome = require('google-home-notifier');
var language = 'ja'; // if not set 'us' language will be used
var ambient = require('ambient-lib');

googlehome.device('リビング', language);
googlehome.ip('192.168.xx.xx');

ambient.connect(102, '', '');

ambient.read({n: 1}, function(err, resp, data) { // 気象センサーの最新データーを取得
    var temp = Math.round(data[0].d1 * 10) / 10; // d1に温度が設定されている(小数点以下1桁で四捨五入)
    try {
        googlehome.notify('そとの温度は' + temp + '度です', function(notifyRes) {
            console.log(notifyRes);
        });
    } catch(err) {
        console.log(err);
    }
});

チャネルID 102は公開チャネルで、データーはこのページで見ることが出来ます。公開チャネルは第3引数のリードキーを指定しなくてもデーターを取得することが出来ます。

いろいろな場所のデーターを測定するには、電池で駆動し、無線でデーターを送信する端末が便利です。このようなセンサー端末からデーターを送信する先としてAmbientは簡単に使うことができます。

Google Homeに話しかけて、センサー値を言ってもらう

ここまでで、センサーからAmbientに送られた値をnode.jsで読み出し、Google Homeに送って言ってもらうことができました。次は、Google Homeに話しかけて、センサー値を言ってもらいます。

Google Homeに声で指示するのはIFTTTを使います。「Google Home IFTTT」で検索するといろいろな事例がでてきます。「Google HomeをIFTTTと組み合わせてTwitterやLineなどと連携しよう!」などを参考に、IFTTTの登録などをおこないます。

Raspberry Pi上のプログラムの拡張

IFTTTには外部のサービスを起動するアクションとしてWebhooksという仕組みが用意されています。指定したURLにHTTP GETやPOSTなどのメッセージを送ることができます。そこで、Raspberry Pi上のプログラムを、HTTP POSTメッセージを受け取ったらセンサー値を読み出し、google-home-notifierを使ってGoogle Homeに送って、センサー値を言ってもらうように拡張します。google-home-notifierをインストールした時にnode_modules/google-home-notifierの下にダウンロードされたexample.jsというプログラムを参考にしました。

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 = 8080;
var ambient = require('ambient-lib');

var deviceName = 'Google Home';
var language = 'ja';
googlehome.device(deviceName, language);
googlehome.ip('192.168.xx.xx');

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

app.post('/google-home-notifier', urlencodedParser, function (req, res) {
    if (!req.body) return res.sendStatus(400);

    ambient.connect(102, '', '');

    ambient.read({n: 1}, function(err, resp, data) { // 気象センサーの最新データーを取得
        var temp = Math.round(data[0].d1 * 10) / 10; // 温度(小数点以下1桁で四捨五入)
        var text = 'そとの温度は' + temp + '度です';
        try {
            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);
        }
    });
})

app.listen(serverPort, function () {
    ngrok.connect(serverPort, function (err, 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');
    });
})

RPiは普通、Wi-Fiルーターなどの内側に置かれるので、HTTPサーバーを立ち上げてもIFTTTなどのクラウドサービスからアクセスすることができません。このプログラムはngrokというサービスを使ってクラウドからルーターの内側のHTTPサーバーにアクセスしています。従って、最初に書いた構成図は、正確に書けばIFTTTとRPi上のHTTPサーバーの間にngrokが入りますが、省略しています。

このプログラムを起動すると、次のようなメッセージが出力されます。

$ node server.js &
*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
何行かWARNINGが出ます
POST "text=Hello Google Home" to:
    http://localhost:8080/google-home-notifier
    https://80d199aa.ngrok.io/google-home-notifier
example:
curl -X POST -d "text=Hello Google Home" https://80d199aa.ngrok.io/google-home-notifier

試しに次のようにcurlコマンドを入力すると、Google Homeが外の温度を言ってくれます。

$ curl -X POST https://80d199aa.ngrok.io/google-home-notifier

IFTTTの設定

次にIFTTTの設定をします。IFTTTにログインし、「My Applets」ページの「New Applet」をクリックし、「if this then that」のthisの部分を作ります。まず「Google Assistant」を選択します。Google Homeに言う言葉は「外は何度?」なので、「Say a simple phrase」を選び、「OK、グーグル」の次に言う言葉を設定します。

IFTTT1

次に「if this then that」のthatの部分を作ります。サービスとしてWebhooksを選択します。URLにRPiで動かしたserver.jsが出力したexampleの上に書かれたアドレスを入力し、MethodはPOST、Content Typeはtext/plainを選択します。Bodyは空欄のままで、「Create action」をクリックします。

IFTTT2

最後に「Finish」をクリックしてIFTTTの設定は完了です。

動作確認

Google Homeに「OK、グーグル、外の温度は?」と聞くと、「はい、外の温度は14.5度です」と答えてくれました。

ちなみに、RPi上のプログラムで、言ってもらう文章を

node.js
        var text = '外の温度は' + temp + '度です';

としていたら、「ガイの温度は14.5度です」と言ったので、プログラムは

node.js
        var text = 'そとの温度は' + temp + '度です';

とひらがなに修正しました。ご愛嬌ですね。

複数のセンサーを扱う

IFTTTの「Google Assistant」には「Say a simple phrase」の他に「Say a phrase with number」など4つの入力パターンがあります。この中の「Say a phrase with a text ingredient」を使うと、センサーが複数ある場合、次のようにセンサーを選んで、そのセンサーの値を言ってもらうようにできます。

私:「ねぇ、グーグル、『外』は何度?」
GH君:・・・ベランダの温度センサーの値を読み取って・・・
  「外の温度は14度です」
私:「ねぇ、グーグル、『リビング』は何度?」
GH君:・・・リビングの温度センサーの値を読み取って・・・
  「リビングの温度は20度です」

Raspberry Pi上のプログラムの拡張

簡単な実験で、「Say a phrase with a text ingredient」を使った場合、POSTメッセージのbody部分に次のような形で「言った言葉」が渡されることを確認しました。

node.js
{ '言った言葉': '' }

そこで、RPi上のserver.jsを次のように拡張しました。

server-multisensors.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 = 8080;
var ambient = require('ambient-lib');

var deviceName = 'Google Home';
var language = 'ja';
googlehome.device(deviceName, language);
googlehome.ip('192.168.xx.xx');

var sensors =
    [
        {
            name: ['そと', ''],
            channelId: 102,
            readKey: 'リードキー'
        },
        {
            name: ['リビング'],
            channelId: チャネルID,
            readKey: 'リードキー'
        },
        {
            name: ['寝室'],
            channelId: チャネルID,
            readKey: 'リードキー'
        },
    ];

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

app.post('/google-home-notifier', urlencodedParser, function (req, res) {
    if (!req.body) return res.sendStatus(400);
    for (key in req.body) {
        for (var i = 0; i < sensors.length; i++) {
            if (sensors[i].name.indexOf(key) >= 0) {
                var sensor = sensors[i];
                ambient.connect(sensor.channelId, '', sensor.readKey);

                ambient.read({n: 1}, function(err, resp, data) { // 気象センサーの最新データーを取得
                    var text = '';
                    var temp = Math.round(data[0].d1 * 10) / 10; // 温度(小数点以下1桁で四捨五入)
                    var created = new Date(data[0].created); // 測定時刻
                    var now = new Date(); // 現在時刻
                    var minuteDistance = Math.round((now.getTime() - created.getTime()) / (60 * 1000));
                    if (minuteDistance > 6 && minuteDistance <= 35) { // 測定時刻が35分以内なら
                        text = minuteDistance + '分前の';
                    } else if (minuteDistance > 35) { // 35分以上前なら
                        text = (created.getMonth() + 1) + '' + created.getDate() + '' + created.getHours() + '' + created.getMinutes() + '分の';
                    }
                    text += sensor.name[0] + 'の温度は' + temp + '度です';
                    try {
                        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);
                    }
                });
            }
        }
    }
})

app.listen(serverPort, function () {
    ngrok.connect(serverPort, function (err, 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');
    });
})

センサー情報(名前、AmbientのチャネルID、リードキー)の配列sensorsを作り、IFTTTから渡されたキーワードからセンサーを見つけ、そのセンサーの値をAmbientから取得してGoogle Homeに言ってもらうプログラムです。

IFTTTの設定

「New Applet」をクリックし、「Google Assistant」、「Say a phrase with a text ingredient」を選択します。グーグルに言う言葉は次のように設定しました。

IFTTT3

アクションに渡す言葉は「\$」で指定します。「OK、グーグル。『外』は何度?」と聞きたいので、「\$ は何度?」と指定したいのですが、「\$」を先頭に書くとエラーになって受け付けてくれません。しかたがないので、先頭に「ところで」をつけて、「ところで \$ は何度?」と指定しています。英語だと命令文や疑問文の先頭は動詞や疑問詞なので問題ないのでしょうが、日本語では使いにくい制限ですね。

アクション部分は次のように設定します。

IFTTT4

URLはRPiで動かしたserver-multisensors.jsが出力したexampleの上に書かれたアドレスを入力し、MethodはPOST、Content Typeはapplication/x-www-form-urlencodedを選択します。BodyはAdd ingredientをクリックし、TextFieldを選択します。

最後に「Create action」、「Finish」をクリックしてIFTTTの設定は完了です。

動作確認

こんな感じのやりとりが実現できました。

フロー

上の図のような動作をしています。

  1. 「OK、グーグル。ところで『外』は何度?」と聞く
  2. Google HomeからIFTTTのトリガーがかかる
  3. IFTTTのアクションでRPi上のHTTPサーバーにPOSTメッセージが送られる
  4. Ambientからセンサー値を取得
  5. google-home-notifierでセンサー値を文章にしてGoogle Homeに送る
  6. Google Homeが「外の温度は10度です」と答える

最後に

Google HomeとIFTTT、google-home-notifierを組み合わせて、センサーの値を言ってもらうことができました。実際に使ってみると、割と普通の会話でセンサーの最新値を知ることができ、思った以上に便利です。パソコンやスマホなら、ブラウザーを起動して、Ambientサイトにログインして、チャネルのページを選択して、センサーの値を見ますが、その操作が声のやり取りに置き換わるので、非常に簡単です。

今回はGoogle Homeに話しかけて、答えを言ってもらいましたが、センサーの値を観測していて、ある条件になったらGoogle Homeに言ってもらうこともできます。例えば部屋の温度と湿度を常に観測し、熱中症になりそうな条件になったら「クーラーをつけてはどうでしょう?」といったアラートを喋ってもらうこともできます。

声のインタフェースは思った以上にいろいろな展開が期待できそうです。

32
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?