月別アーカイブ: 2017年11月

GoogleHomeで潮汐情報 その7

GoogleHomeに潮汐情報をしゃべらせるようにして満足はしていたのですが、使っていると面倒なことも出てくるもので・・・

大きくは二つです
 ・明日の名古屋は?ときいても、2017年11月10日の・・・みたいに言われて長い。
 ・そもそも、今いるところに近いところの情報が聞きたいわけで、名古屋は?とか面倒。

一つ目は、「明日の」と聞いているので、そのまま「明日の」としゃべってもらえればよいです。リクエストのJSONを見てみると

  ["result"]=>
  object(stdClass)#15 (10) {
    ["source"]=>
    string(5) "agent"
    ["resolvedQuery"]=>
    string(27) "明日の名古屋の潮は"
    ["speech"]=>
    string(0) ""
    ["action"]=>
    string(9) "calc_tide"
    ["actionIncomplete"]=>
    bool(false)
    ["parameters"]=>
    object(stdClass)#16 (2) {
      ["date"]=>
      string(10) "2017-11-10"
      ["geocity"]=>
      string(9) "名古屋"
    }
    ["contexts"]=>
    array(3) {
      [0]=>
      object(stdClass)#17 (3) {
        ["name"]=>
        string(32) "actions_capability_screen_output"
        ["parameters"]=>
        object(stdClass)#18 (4) {
          ["date"]=>
          string(10) "2017-11-10"
          ["date.original"]=>
          string(6) "明日"
          ["geocity.original"]=>
          string(9) "名古屋"
          ["geocity"]=>
          string(9) "名古屋"
        }
        ["lifespan"]=>
        int(0)
      }

result->contexts[0]->parameters->date.originalに情報があるので、それが使えそうです。収集してそれを出すように変えます。

if($action == "calc_tide") {
        $date = $json->result->parameters->date;
        list($year, $month, $day) = split("-", $date);
        $portName = $json->result->parameters->geocity;
        $dateStr = $json->result->contexts[0]->parameters->{'date.original'};
        $message = createTideMessage($year, $month, $day, $dateStr, $portName);
        speak($message);
}

これは簡単ですね。

次に、位置情報です。位置情報はFirebaseを使う場合のサンプルはあるのですが、JSONを自分で触る時の情報は全然ないのですよね・・・

まず、Default Welcome IntentをWebHookを使うようにします。Defaukl Welcome Intentは、プログラム側で受ける場合には、input.welcomというアクションになるのですが、これで緯度経度を取得したいという権限がほしいというJSONを返すようにします。

if($action == "calc_tide") {
        $date = $json->result->parameters->date;
        list($year, $month, $day) = split("-", $date);
        $portName = $json->result->parameters->geocity;
        $dateStr = $json->result->contexts[0]->parameters->{'date.original'};
        $message = createTideMessage($year, $month, $day, $dateStr, $portName);
        speak($message);
} else if($action == "input.welcome") {
        echo '
                {
                "speech": "PLACEHOLDER_FOR_PERMISSION",
                "data": {
                    "google": {
                        "expectUserResponse": true,
                        "isSsml": false,
                        "noInputPrompts": [],
                        "systemIntent": {
                            "intent": "actions.intent.PERMISSION",
                            "data": {
                                "@type": "type.googleapis.com/google.actions.v2.PermissionValueSpec",
                                "optContext": "近くの港を検索するため",
                                "permissions": [
                                        "DEVICE_PRECISE_LOCATION"
                                ]
                            }
                        }
                    }
                }
        } ';
}

返すJSONについては、この辺りが詳しいです。

今回は緯度経度だけなので、DEVICE_PRECISE_LOCATIONを指定していますが、NAMEを指定すると名前が、DEVICE_COARSE_LOCATIONを指定すると住所が取得できます。

これで、Google Assistantに接続したときに、情報を取得してよいか?というメッセージが表示されます。

IntentでDefault Welcome IntentのFollowUp Intentを作ります。このFollowUp Intentで、情報取得してよいか?の結果を受け取ります。Default Wlecome Intentの右の方にカーソルを持っていくと、Add follow-up intentというのが表示されますので、それをクリックします。

クリックするとメニューが表示されますので、fallbackを選択します。

中を変更する必要はありません。必要であれば、WebHookを呼んでサーバ側に登録するというのもありかもしれません。

次に、内部的には、actions_intent_PERMISSION上記の結果を受けるIntentを追加します。

Eventsにactions_intent_PERMISSIONというイベントを追加します。
これにもWebhookを追加しておきます。
このイベントは、action名を、set_locationとしておくことにします。

これで、接続(action:input.welcome) →パーミッション取得 → パーミッション取得イベント(action: set_location)という流れができました。

PHP側にもコードを追加します。

if($action == "calc_tide") {
        $date = $json->result->parameters->date;
        list($year, $month, $day) = split("-", $date);
        $portName = $json->result->parameters->geocity;
        $dateStr = $json->result->contexts[0]->parameters->{'date.original'};
        $message = createTideMessage($year, $month, $day, $dateStr, $portName);
        speak($message);
} else if($action == "input.welcome") {
        echo '
                {
                "speech": "PLACEHOLDER_FOR_PERMISSION",
                "data": {
                    "google": {
                        "expectUserResponse": true,
                        "isSsml": false,
                        "noInputPrompts": [],
                        "systemIntent": {
                            "intent": "actions.intent.PERMISSION",
                            "data": {
                                "@type": "type.googleapis.com/google.actions.v2.PermissionValueSpec",
                                "optContext": "近くの港を検索するため",
                                "permissions": [
                                        "DEVICE_PRECISE_LOCATION"
                                ]
                            }
                        }
                    }
                }
        } ';
} else if($action == "set_location") {
        $tide = new tide_base();
        if(isset($json->originalRequest->data->device)) {
                $lat = $json->originalRequest->data->device->location->coordinates->latitude;
                $lng = $json->originalRequest->data->device->location->coordinates->longitude;
                $nearPort = $tide->getNearPort($lat, $lng);
                $nearPortAll = $tide->getNearPortAll($lat, $lng);
                $port = $tide->minatoAll;
                $nearPortName = $port[$nearPort][0];
                $nearPortNameAll = $port[$nearPortAll][0];
                if($nearPort == $nearPortAll) {
                        $message = "一番近い港は" . $nearPortName . "です。";
                        $message = createTideMessage($year, $month, $day, "今日", $nearPortName);
                } else {
                        $message = "一番近い港は" . $nearPortNameAll . "ですが主要4分潮の計算しかできません。60分潮>の計算ができる一番近い港は" . $nearPort + "です。";
                        $message = createTideMessage($year, $month, $day, "今日", $nearPortNameAll);
                }
        } else {
                $message = "登録なしで進めます。";
        }
        speak($message);

}

さてテストしてみましょう。

沓形ってどこ?

画面上小さくなっているため見えないのですが、デフォルトではシミュレーターの場所が、MountainViewになっていますね。右上のLocationで変更してやります。
ちなみに、MountainViewからだと、沓形が一番近いのかな?

場所を東京にして見ます。

いい感じにできました!

GoogleHomeで潮汐情報 その6

前回までで、満潮、干潮をしゃべらせることができました。
ただ、GoogleHomeにむかって、テスト用アプリにつないでというのはどうしてもいただけません。

そこで、もう少しちゃんと体裁を整えましょう。

Action on Googleの画面に行き、プロジェクトを選択します。

現在は、Actionの登録の途中ですが、App Infomationを入れてみます。

ADDで追加します。

言語を日本語にして、NameとPronunciationを入れます。

その後、Nextをクリックし、詳細を入れます。

イメージ画像など、必要な情報を登録していきます。
ローカルでテストするだけなので、フリー素材などを利用すれば良いでしょう。
それ以外にも記載する内容がありますので、記載していきます。

全て入力終わったら、SAVEします。言語が英語の方でエラーが出る場合は、英語側も入力します。
ほんらいは、英語側は削除したかったのですが、削除の仕方がわかりませんでした。

完了すると、Action on Googleの画面でTESTが実行可能になりますので、TEST DRAFTを実行します。

ここで、「Your app must have at least one action for locale en」というエラーが起きてしまいました。
これは、Dialogflow側で、jaのアクションしか作っていないのが原因です。

そこで、Dalogflow側で追加します。

jaの横の+をクリックします。

Select Additional Languageで、English – enを追加し、SAVEします。

localeが追加されました。今度は、Dialogflow側でテストを実行します。
メニューのIntegrationsから、Google Assistantを選び、テストを実行します。

テストが成功したら、VIEWをクリックしシミュレーターでテストします。

最初だと、言語が日本語になっていないので、日本語に変えてテストをします。

できました。

GoogleHomeで潮汐情報 その5

前回は、firebaseで外に出れず悩んだところまで行きました。
JavsScript移植するか、どうするか悩んだ結果、WebHookのREST APIを直接飲んでしまえばいいことに気づき、潮汐情報を取得するAPIに仕立てることにしました。

いまさらPHPなんて使いたくないのですが、手っ取り早いのでそのままスタートしてみることにします。

メニューのFulfillmentからWebhookを有効にします。そして、URLを記載します。このとき、URLはhttpsである必要がありますので注意してください。

こうすると、Intentのページにfulfillmentのセクションが追加されます。

ここで、Use Webhookとするとことで、このIntentに対してWebhookが呼び出されます。

次に、このURLに該当するAPIを書き始めます。おそらく、JSONが飛んできているはずなので、JSONをダンプしてみることにします。

<?PHP

$entityBody = file_get_contents('php://input');

ob_start();
var_dump(json_decode($entityBody));
$result =ob_get_contents();
ob_end_clean();

$fp = fopen("/tmp/action-on-google-dump.txt", "a+" );
fputs($fp, $result);
fclose( $fp );

フレームワーク?なにそれ? という感じでべた書きです。

シミュレーターでテストしてみます。

シミュレーター上の戻りは同じですが、APIが呼ばれたサーバにはJSONが飛んできていることが確認できました。

その内容を見てみるとわかるのですが、resultのセクションに値があります。

  ["result"]=>
  object(stdClass)#15 (10) {
    ["source"]=>
    string(5) "agent"
    ["resolvedQuery"]=>
    string(27) "明日の名古屋の潮は"
    ["speech"]=>
    string(0) ""
    ["action"]=>
    string(9) "calc_tide"
    ["actionIncomplete"]=>
    bool(false)
    ["parameters"]=>
    object(stdClass)#16 (2) {
      ["date"]=>
      string(10) "2017-11-10"
      ["geocity"]=>
      string(9) "名古屋"
    }

パラメータに指定した、dateに2017-11-10が、geocityに名古屋がはいっていますね。
「明日の名古屋の潮は」と言ったのにちゃんと日付に変換されているところがいいです。

また、もう一つ注意すべきのはactionです。ここで、Intentに指定したActionの名前が入っています。

これがわかれば、あとは応答を返すだけです。とはいっても、どう返せば・・・と調べたところ、一番シンプルな応答は次のようになるようです。

{
  'speech': 'しゃべってほしいメッセージ',
  'displayText': '画面に表示するメッセージ',
  'source': '適当な名前'
}

というわけで、ちょっと書き換えます。

$entityBody = file_get_contents('php://input');
$json = json_decode($entityBody);

$action = $json->result->action;

function speak($message) {
        echo "
                {
                'speech': '$message',
                'displayText': '$message',
                'source': 'webhook-tide'
                }
        ";
}

if($action == "calc_tide") {
        $date = $json->result->parameters->date;
        list($year, $month, $day) = split("-", $date);
        $portName = $json->result->parameters->geocity;
        $message = sprintf("%sの%sの情報です。", $date, $portName);
        speak($message);
}

日付と港の名前を取得して、返すようにしています。

いいですね! うまくいっています。

あとは、これをちゃんとした情報に変えてやればOKです。

if($action == "calc_tide") {
        $date = $json->result->parameters->date;
        list($year, $month, $day) = split("-", $date);
        $portName = $json->result->parameters->geocity;
        $message = createTideMessage($year, $month, $day, $portName);
        speak($message);
}

createTideMessageは、独自に持っている、情報をメッセージで取得する関数です。

うまくいきました!

年を言わなくても、ちゃんと見つけてくれます。(年を言っていないのに、来年になっているのがいいですね!)

GoogleHomeで潮汐情報 その4

使い物になるかどうかは別として、前回まででとりあえずGoogle Assistantでアプリが動きました。この状態でも、GoogleHomeに「テスト用アプリにつなげて」としゃべりかければ
利用ができると思います。

ただ難点が・・・ アプリが終わりません。

なので、まずアプリを追えるIntentを追加することにします。

CREATE INTENTから・・・

「アプリを終了する」を追加します。追加で「終了」だけも追加しておくことにします。
そして、一番下の、Google Assistantから、「End conversation」にチェックを入れます。

これで終了できるようになりました。

さて、次は潮時を出力する番です。
今手元にあるプログラムはかなり前に書いたこともあり、PHPで書かれています。
よくあるサンプルでは、firebaseを使って開発をする流れになるのですが、この場合だと、JavaScriptに移植しなければいけません。単純なプログラムならいいのですが、大量の計算式を移植しなければならず、とても簡単に移植できそうな気がしません。

そこで、PHPの方で情報を返すJSONを返すREST APIぽく仕立て、firebaseからこのREST APIをたたく仕組みを考えてみました。

つまり、GoogleHome → Firebase → PHP REST呼び出しという感じですね。

・・・が結果はNG.どうも有料バージョンでないと、firebaseから外に出ていけないようです。さて困りました。

次に続きます

GoogleHomeで潮汐情報 その3

前回まででいったんIntentを追加しましたので、ここでテストをしてみることにします。

Dialogflowのメニューから、Integrationsを選択します。

Action Assistantを選択します。

公開に必要な情報の入力になります。公開はしませんが、Test your appを実行したいので、一番下のAUTHORIZEをクリックします。

Googleの認証が入りますので選択して先に進みます。

画面が変わりますので、「TEST」を実行します。


するとテストが実行され、Google assistantで実行できるか確認され、実行可能であれば下にTest now activeが表示されます。これで、VIEWをクリックすすればテスト可能です。

この時複数アカウントを利用しているとちょっと面倒です。というのも、デフォルトアカウントでシミュレーターに入ってしまうので、そのままだと表示されません。その場合は、URLの/u/の後の数字をうまく操作すれば利用できるようになるはずです。

シミュレーターが動くと、Input欄に「テスト用アプリにつないで」というメッセージがすでに入っています。そのまま、マウスカーソルを持っていき、Enterキーでテスト用アプリにつなぐことができます。

いきなりエラーが発生しました。
なぜか、最初だとだめなんですよね・・・ しばらくしてから再読み込みしてみたところ、うまく接続できました。リクエストを見るとロケールがja_JPに変わっているので、これが原因なのかな。

「こんにちは!」が返ってきていますね。それでは、明日の名古屋の情報を聞いてみましょう

指定されたメッセージが返ってきていますね。

次回でAPIに接続して、実際の情報を取得してみましょう。

GoogleHomeで潮汐情報 その2

前回は潮汐情報のプロジェクト作成まで行いましたので、ここからIntentの追加を行います。

今最初に見えている、Default Welcome Intentが、このアプリに接続をした時に言われる最初のメッセージになります。デフォルトでは、「こんにちは!」が入っていますので、まぁ、これはこのままにしておきます。

次に「潮汐を知りたい」という意図を記載します。

CREATE INTENTをクリックします。

User saysに、会話のトリガーを記載します。まずは「今日の名古屋の潮汐は」というキーワードで入れます。

その後、「今日」という文字をマウスで選択すると、ウインドウが開きどの名前に割り当てるかを聞いてきます。日付に該当するので、@sys.dateに割り当てます。

そうすると、「今日」という部分は日付が入る部分だと認識してくれるわけです。これで、「今日」の部分が「明日」になっても「11月21日」とか日付になっても正しく認識されます。

同じように、「名古屋」の部分を「@sys.geo-city」にします。User saysのところに入れて、Enterキーを押すと確定します。

確定すると、Actionのところにパラメータが入ります。

ただ、実際、「今日の名古屋の潮汐は」なんて「潮汐」なんて言葉使いません。なので、別の言い方も登録しておきます。

次に、Actionを追加します。Action nameはこの後呼び出されるAPIに渡される関数に利用されます。その下のパラメータがプログラム側にわたる情報になります。

action名はcalc_tideとします。また、パラメータ名のgeo-cityいう名前に-が入っているので、これを除去しておきます。ハイフン入りのパラメータは扱いにくいので・・・

さらに、date, geocityともに必須にします。必須にすると、入っていなかったときに問い合わせるためのPROMPTSが指定できるようになりますので、これも合わせて指定します。

一旦テストのために、テキストメッセージを入れておきましょうか。メッセージは、Text responseに入れます。

最後にページ上部のSAVEで保存完了です。IntentNameは入れなくても自動で入ります。

さらに次回に続きます。次は一旦テストをしてみることにします。

GoogleHomeで潮汐情報 その1

今回は、釣り竿と違う話ですが・・・

みなさん、その日の潮汐情報ってどのように確認してるでしょうか? 釣具屋さんとかで潮時表をもらったりしてそれ見ていたり、インターネットで調べたりすることが多いと思います。
私はこの潮汐情報を確認するのが面倒なので、何とか楽に確認したいと以前から考えていました。

まぁ、それもあって、以前(2011年ごろ)潮汐を取得するプログラムを作りました。潮汐の計算の元ネタは気象庁の潮位表からもらってきていますが、これらのパラメータを主要な港では60分潮、そのほかの港では4分潮の重ね合わせの計算を行うと潮位がわかる仕組みになっています。

こういったシステムや、Webで調べるのが今のところ一番早いのですが、せっかくGoogleHomeを買ったので、声で問い合わせできないか?と思い立ち、テストをしてみることにしました。

GoogleHomeでのアプリを作るには、Action on Googleという仕組みを使います。ここからアクションを作るのは、いろんなページにサンプルがあるのでそれを参考にしてもらえればよいかと思います。

たとえば、公式ドキュメントのBuild Your First App with Dialogflowなんかもわかりやすいですし、Qiitaとかでも、そのまま実行すればある程度のものは作成できます。私も備忘録を兼ねて書いておこうと思います。

基本的には、Action On Googleでプロジェクトを作成→Dialogflowでアクションを追加というような流れになります。

まずはAction On Googleでプロジェクト作成を行います。

そして、DialogflowをBUILDします。

CREATE ACTIONS ON DIALOGFLOWをクリックしてアクション作成を開始し始めます。

DialogFlowのリクエスト許可が必要になりますので、許可します。

DialogFlowにも情報が必要になりますので入力します。言語は日本語に設定してください。

Saveが終了すると、Intentの設定画面に変わります。ここからがスタートです。

長くなりそうなので、何回かに分けます。