GoogleAppsScript
APIGateway
GoogleAssistant
GoogleHome
dialogflow

ねんがんのgoogle homeをてにいれたぞ

GAFA1の中でと言わず星の数ほどある企業の中でgoogleが大好きな私が、満を持してgoogle homeを買いました。

最初にぶつかった壁

Play Musicは有料版を使わないと再生すらできない!
アップロードしてあるのに・・・。

信者なのでお布施と割り切って、サインアップします。

第2の壁

妻が大好きなミスチルを再生しようと思うと、ラジオミックスとかMobile Melody Seriesに邪魔されて狙った曲が再生できない。
妻にお小言をいただく。おうふ。。。
プレイリストを作ってやりくりして許してもらいました。

第3の壁

chromecastにつないだ。
・・・ぶっちゃけスマホ操作の方が楽です。

閑話

使い道が見つからない。
子供がおもちゃにして遊んでるのでまあいいのだけど。
下の子がわざと「ねぇぐるぐる」と話しかけて反応を見て喜んでいる姿に癒やされる。

最大の壁

使い道を広げようとIRKitを買おうと思っても、時すでに遅し。
品薄どころか手に入らない。
うーん、このまま埃をかぶるのはもったいない。
まて、お前の本懐はなんだ! と自問したところで、ガジェットをいじり倒すのが好きなことを思い出す。

本題

前置きが長くなったところで、本稿の本題です。
自分でアプリを作ると言ってもアイデア勝負なので苦手とするところ。
ピコーンと頭に浮かぶ、近づいてくる冬休み。
私に似て最終日に宿題をまとめて必死にやる娘のために、彼女にとって旬なgoogle homeで宿題管理アプリを作ればやる気を出すかも?

使うもの

  • Actions on Google
  • Dialogflow
  • AWS API Gateway & Lambda
  • Google Spreadsheet with GAS2

構成

000003.JPG
こんな感じ

それぞれの内容

裏側から順に

Spreadsheet with GAS

000004.JPG
スプレッドシートをDB代わりに使います。
くくカードとは九九を覚えるための単語帳みたいなカードです。
九九と書くと「きゅうじゅうきゅう」と発声してしまったのでひらがなで書いてます。

スクリプトはこんな感じ

function doPost(e){
  var request = JSON.parse(e.postData.getDataAsString());
  switch(request.result.parameters.action){
    case 'list':
      var list = getList();
      var result = {"list": list, "speech": convertListToSpeech(list) }
      return returnAsJSON(result);
    case 'start':
      var target = request.result.parameters.target;
      return returnAsJSON(startTask(target));
    case 'end':
      var target = request.result.parameters.target;
      return returnAsJSON(endTask(target));
    default:
      return returnAsJSON({"returnCode":"no operation", "speech": "ごめんね。よくわからなかったよ"});
  }
}

function convertListToSpeech(list){
  return list.join("と") + "が残ってるよ";
}

function startTask(name){
  var remained = getRemainedTasks();
  var continued = remained.filter(function(e){ return (e[1] && !e[2]); });
  var targetTask = remained.filter(function(e){ return e[3] == name });
  var result = {}
  if (continued.length > 0) { 
    result.speech = continued[0][0] + "がまだ終わってないよ";
  }else if(targetTask.length == 0){
    result.speech = "あれ? 終わってるのかな? その宿題はもうないよ";
  }else{
    setValue(name, 1, new Date());
    result.speech = "がんばってね!";
  }
  return result;
}

function endTask(name){
  var remained = getRemainedTasks();
  var continued = remained.filter(function(e){ return (e[1] && !e[2]); });
  var targetTask = remained.filter(function(e){ return e[3] == name });
  var result = {}
  if (continued.length > 0) { 
    setValue(name, 2, new Date());
    result.speech = "すごい! よく頑張ったね";
  }else if(targetTask.length == 0){
    result.speech = "あれ? 終わってるのかな? その宿題はもうないよ";
  }else{
    setValue(name, 2, new Date());
    result.speech = "すごい! よく頑張ったね。始める前に教えてほしかったな。";
  }
  return result;
}

function getList() {
  var remained = getRemainedTasks();
  var result = remained.map(function(e){ return e[0]; });
  return result;
}

function getRemainedTasks() {
  var sheet = SpreadsheetApp.openById("XXXXXXXXXXXXXXX").getActiveSheet(); // IDはシートのID
  var range = sheet.getDataRange();
  var values = range.getValues();
  var result = values.filter(function(e){ return !e[2]; });
  return result;
}

function setValue(name, target, value){
  var sheet = SpreadsheetApp.openById("XXXXXXXXXXXXXXX").getActiveSheet(); // IDはシートのID
  var range = sheet.getDataRange();
  var values = range.getValues();
  for (var i = 0; i < values.length; i++){
    if (values[i][3] == name){
      sheet.getRange(i+1, target+1).setValue(value);
    }
  }
}

function returnAsJSON(obj){
  return ContentService.createTextOutput( JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}

これをWebサービスとしてデプロイしています。
このときにかなりはまったのが、修正したら都度新版でデプロイする必要があること。
どうしても修正が反映されずあほみたいな時間を過ごしました。

AWS

API Gatewayには特別なことはやっていないので設定等は割愛。
ググれば腐るほど出てきますね。

肝心のLambdaの処理は、なんと・・・

import urllib.request, json

def lambda_handler(event, context):

    url = "https://script.google.com/macros/s/XXXXXXXXXXXXXXXXX/exec" # GASのエンドポイント
    headers = {"Content-Type": "application/json"}
    body = json.dumps(json.loads(event["body"])).encode("utf-8")

    request = urllib.request.Request(url, data=body, method="POST", headers=headers)
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")
    result = {"statusCode": 200, "body": response_body }

    return result

右から左へ受け流す3だけ。

なぜこんなことをしているのかというと、GASでデプロイしたエンドポイントは、302リダイレクトをしてしまい、DialogflowのWebhookとして使えなかったからです。
最初に記事を書いていたときには、「Google大好き! GASとActions on GoogleでGoogle Homeをハックする」でした。。
Cloud functionsを使わなかったのは、単に慣れの問題です。

Dialogflow~Assistant

API gateway ~ Dialogflow ~ Assistantの設定も良い記事が多いので詳細は割愛します。
どうなったかというところですが
000005.JPG
こんな感じでインテント作って
000006.JPG
こんな風にインテグレーションを設定すれば意図した通りに動きました。
ポイントは、ほぼ何もしないWelcomeインテント(宿題管理)を作って、そこから先の詳細処理をAdditional triggersとして設定したことでしょうか。

結果

000007.JPG
000008.JPG

なんかラグがあったのでしょうか? よくわからないけど、とりあえずできた!
冬休みなので虫取りはしません。
アプリ名のダサさが私の個性です。
マンと言いつつ女声です。

まとめ

google homeのアプリを作るのはかなりお手軽でした。
子供が使って楽しめるように拡張していきたいと思います。
宿題のタスクを登録できるようにするとか、回答バリエーションを増やすとか、いろいろやれることがありますね。

お知らせ

まだまだgoogle homeが仕事を肩代わりしてくれないので、エイチームブライズでは仕事をいっしょに楽しむマンを募集中です。
興味のある方はぜひともエイチームグループ採用ページWebエンジニア詳細ページ)よりお問い合わせ下さい。

明日は

デザイナーチームを華やかに彩るうちの1人@mamitasoが参戦です。
記事の企画を聞きましたがデザイナーならではの面白いものが期待できます。
お楽しみにしてください!

脚注


  1. MSやIBMがここに入らないということに時代の流れを感じる 

  2. GASについては@rf_pが多くの記事を書いているのでそちらをご参考に 

  3. ムーディ、意外とクセになりましたよね