Fablog

社会人マイナス1年生のブログ / プログラミング / 料理 / ビットコイン

GoogleHome のセットアップ方法

概要

GoogleHome を買ってセットアップをしたのでその方法をまとめとく

 

GoogleHomeとは

Google の出しているスマートスピーカー。

アメリカではだいぶ前から出ているけど、最近日本語に対応してCMがバンバンながれるようになった。

検索、目覚まし、家電操作、なんでもござれの凄いやつ。

PVをみるとそのすごさがなんとなく分かる。

 

www.youtube.com

 

セットアップ

特にやることはなかった。

電源を繋いだあとは、スマホのアプリのなかでやる。

 

play.google.com

 

このアプリを入れると端末をセットアップしますか?みたいなやつが出てくるのでそこをタップする。

あとはアプリのガイダンスに乗っ取って設定を進めていくだけ。

 

動いたよ

www.youtube.com

 

 

玄関のスマートロックができるQrioのセットアップの方法

概要

Qrioというスマートロックデバイスを買ったのでそのセットアップ手法をまとめておく

 

Qrioとは

ベンチャーキャピタルのWiLと、世界的な電機メーカーのソニーとのジョイントベンチャーとして設立されたQrio株式会社が出しているスマートロックデバイス

仕組みとしては鍵のつまみの箇所に取り付けて、つまみの開閉をIoTデバイスを通じて自由にできるようにするモノっぽい

Qrioを使うとこんなことができる。

  • 鍵の箇所に被せるだけで設置完了
  • 既存の鍵と並行して使える
  • アプリを介して施錠管理
  • 自動施錠
  • 位置情報を使って自動開錠
  • 鍵のシェア

今回はこのQrioを買ったのでセットアップをしていく。

 

www.youtube.com

 

 

セットアップ方法

amazon で注文したところこんな感じで届いた

f:id:sasa_sfc:20171115164750j:plain

 

開封してみるとこんな感じ。

 

f:id:sasa_sfc:20171115164800j:plain

 

手紙の封筒にはオーナー登録カードと取り扱い説明書が入ってた。

Qrionには電池がなくなったときのための予備電池も入っているらしいが、その分は別途購入する必要がありそう。

大小様々な鍵つまみに対応するためにSMLの複数の部品があったり、高さ調節のための台が2個入っていたりする。

今回はLサイズの鍵鍵つまみで、台は2台とも取り付けた。

 

なんとなく取り付けられそうなきがしたのでアプリを入れて設定をしていく。

 

play.google.com

 

普通に日本語で親切なチュートリアルがあったので、それに乗っ取って設定をして行ったら詰まることなくできた。

 

無事動いたよ

www.youtube.com

 

 

CIでPullRequestの自動レビューを行う

概要

Dangerを使ってプルリクエストの自動レビューを行う

詳細

僕は未熟な人間なのでよくないプルリクを多々投げてしまいます タイポとか、使ってない関数残ってたりとか、記法の間違えとか

そういう小さな間違えで先輩の貴重な時間を使うのは申し訳なさすぎるのでDangerというものを導入しようと思いました Dangerは自動でプルリクにレビューをしてくれるライブラリです

人の手を煩わせなくても指摘できるような小さなミスを機械がレビューしてくれます

今回はこのDangerとeslintていい感じのレビューをしてくれるBotを作っていきます

導入方法

今回はDangerfileでcheckstyle.xml形式のファイルを読み込んでレビューを行うようにするので、eslintcheckstyle.xmlを出力できるように準備しておく

{
 ~~~ 省略 ~~~
  scripts: {
    "lint:checkstyle": "eslint src -f checkstyle -o checkstyle.xml"
  }
 ~~~ 省略 ~~~
}

Gemを使う準備をする

$ bundle init

Gemの設定

# frozen_string_literal: true
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem 'danger'
#checkstyle を読み込んでレビューできるようにするためのGem
gem 'danger-checkstyle_format'

Gem を入れる

$ bundle install

ルートディレクトリでDangerfileを作る

# including in a project's CHANGELOG for example
github.dismiss_out_of_range_messages

checkstyle_format.base_path = Dir.pwd
checkstyle_format.report("${your-path}/checkstyle.xml")

circleciでdangerを実行するように設定します このときにDANGER_GITHUB_API_TOKENという値を同時に設定しておきます このTOKENの値はGithubsetting -> developper setting -> personal access token で取得できます このTokenの所持者のアカウントでdangerによるレビューが行われるのでBot用のアカウントを作っておくと便利です

machine:
  environment:
    DANGER_GITHUB_API_TOKEN: ${your github api token}
test:
  override:
    - npm run lint:checkstyle
    - bundle exec danger

これで、circle ci でビルドが走るたびに stylecheck.xml が生成され、その値によってレビューがされるようになります

スクリーンショット 2017-10-28 18.37.29.png

すごい!便利!はっぴー!

参考

Dangerで効率よくPR駆動開発 | Recruit Jobs TECHBLOG

github.com

qiita.com

actions SDK Carousel のサンプルコード

github.com

Actions SDK を使っている時にカルーセルの使い方がわからなかったので色々調べていた

結論としては使い方とか全部ライブラリのコードにコメントアウトで書いてあったのだけど

いつかそのことを忘れてググってもすぐでてくるようにメモ

const app = new ApiAiApp({request, response});
const WELCOME_INTENT = 'input.welcome';
const OPTION_INTENT = 'option.select';

function welcomeIntent (app) {
  app.askWithCarousel('Which of these looks good?',
    app.buildCarousel()
     .addItems([
       app.buildOptionItem(SELECTION_KEY_ONE,
         ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2'])
         .setTitle('Number one'),
       app.buildOptionItem(SELECTION_KEY_TWO,
         ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2'])
         .setTitle('Number two'),
     ]));
}

function optionIntent (app) {
  if (app.getSelectedOption() === SELECTION_KEY_ONE) {
    app.tell('Number one is a great choice!');
  } else {
    app.tell('Number two is a great choice!');
  }
}

const actionMap = new Map();
actionMap.set(WELCOME_INTENT, welcomeIntent);
actionMap.set(OPTION_INTENT, optionIntent);
app.handleRequest(actionMap);

actions SDK GoogleHome と スマホ で処理を分けたくなった時に使うTIPS

github.com

actions sdk を使っている時、GoogleHome と スマホ で処理を分けたくなった時に使うTIPS

こんな感じで処理を記述すると、画面の有無を取得することができるので処理を分けることができる

if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) {
  smart phone 用の処理
} else {
  google home 用の処理
}

GoogleAssistantアプリを作るチュートリアル的なものを作りたかった

概要

GoogleAssistantアプリの開発プラットフォームがついに日本語にも対応しましたね

developers.googleblog.com

せっかくなので簡単なアプリをリリースしました

そしてその作ったアプリの過程をまとめて、GoogleAssistantアプリを作るチュートリアルを作りました。。。。

と言いたかったのですがチュートリアルとしては微妙なものができてしまいました

しっかりした解説はないけど、GoogleAssistantアプリを作ったことないけど作りたい人には役立つと思うのでよかったら読んでください

Projectの作成

Actions on Google の Console に行って add Project をします

https://console.actions.google.com/

するとProjectNameとCountryを選ぶ箇所があるので選んでCreateProjectを押してください

するとこんな感じで API.AI を使うか Actions SDK を使うかみたいに選ぶ画面になるので選んでください 今回はAPI.AIを使って行くのでAPI.AIを選んでください

ここで選んだプラットフォームは取り消せないので、間違えて選んでしまった人は新しくプロジェクトを作り直してください

API.AIを選ぶとこんな感じでAPI.AIのプロジェクトを作れよと言われるので作ります

API.AIの画面に行くとdescription, language, timezone などを設定する場所があります。Alarmとかみたいなものを選択できる箇所もあるのですが、日本語は対応していないので、languageで日本語を選択するとその箇所は消えるので埋めなくても大丈夫です。

選択して保存するとこんな感じになります

DefaultIntent

まずは最初に呼ばれるDefaultWelcomeIntentというトリガー的なものを設定します 左のIntentsを選んだあとに default welcome intent を選んでください

その後、GoogleAssistantで起動された時にこのdefault welcome intent が起動するように、Eventsのところに Google_ASSISTANT_WELCOME を入力してください

次に受け答えを設定します UserSaysのところに「こんにちは」 textResponseのところに「こんにちは!フリーブックスです!」 と入力してください

ここで、一度動くかテストをしてみましょう 画面の右端にある Try it now に「こんにちは」と入力してみてください

すると先ほど入力しように「こんにちは!フリーブックスです!」とのレスポンスが返ってきます。

このように、API.AIではユーザーの入力とそのレスポンスを事前に定義しておくことでBotを作ることができます。

しかし、この侭では事前にレスポンスを全て用意しておかなければならず大変です。 そこでAPI.AIのwebhook機能を使います。

CloudFunction の利用

ここで一旦API.AIはおいて、cloudfunctionに移ります

Actions on Google を参考にして ApiAiApp を作って行きます

Build Fulfillment  |  Actions on Google  |  Google Developers

fablog.hatenadiary.com

コードを貼るので詳しい説明は省きますが、基本的にやっていることは

  • appを引数に取るIntentメソッドを作る
  • Intentメソッドの中でいい感じにレスポンス文言を返すようにする
  • Actionと呼ばれる、後々API.AIで定義する文字列とIntentメソッドを関連づける

というだけです。 GoogleHome とそれ以外の端末でレスポンスを分けたりとか、カルーセルの使い方とかを知りたいって方はここを読んでください

fablog.hatenadiary.com

'use strict';
/*
* Modules
*/
const App = require('actions-on-google').ApiAiApp;
const functions = require('firebase-functions');
const request = require('request');
const cheerio = require('cheerio')

/*
* Constants
* 定数系
*/
const BASE_URL = 'https://www.amazon.co.jp/';
const Status = {
welcome: 0
, showBooks: 1
, selectedBook: 2
}

const Actions = {
WELCOME: 'welcome',
SHOW_BOOKS: 'show.books',
CONFIRM: 'confirm',
SELECTED_BOOK: 'selected.book',
}

let options = {
method: 'GET',
url: BASE_URL,
encoding: 'UTF-8',
headers: {
'Content-Type': 'text/plain;charset=utf-8',
'User-Agent': 'Mozilla/5.0(Linux;Android6.0;Nexus5Build/MRA58N)AppleWebKit/537.36(KHTML,likeGecko)Chrome/61.0.3163.100MobileSafari/537.36'
}
}

const books = [{ url: 'https://www.amazon.co.jp//gp/aw/d/B00BB1ZSJA?ref_=msw_best_freehome_0&storeType=ebooks',
title: 'レッツ☆ラグーン(1) (ヤングマガジンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51tbQJsZxLL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075NKCVKD?ref_=msw_best_freehome_1&storeType=ebooks',
title: 'ゴールデンカムイ【期間限定無料】 1 (ヤングジャンプコミックスDIGITAL)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/618tG-9ZD0L._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00BML5V6E?ref_=msw_best_freehome_2&storeType=ebooks',
title: '亜人(1) (アフタヌーンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51VKi59sPPL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075NL61FD?ref_=msw_best_freehome_3&storeType=ebooks',
title: 'ゴールデンカムイ【期間限定無料】 2 (ヤングジャンプコミックスDIGITAL)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61mxKIeQYDL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075TY9SD3?ref_=msw_best_freehome_4&storeType=ebooks',
title: 'アヤメくんののんびり肉食日誌(1)【期間限定 無料お試し版】 (FEEL COMICS)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/513a5UTtm9L._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00KCL6Y2U?ref_=msw_best_freehome_5&storeType=ebooks',
title: 'いぬやしき(1) (イブニングコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51YEu609hIL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075J8HKBZ?ref_=msw_best_freehome_6&storeType=ebooks',
title: 'うわばみ彼女【期間限定無料版】 1 (ジェッツコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51S8gTApfPL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00UN48X98?ref_=msw_best_freehome_7&storeType=ebooks',
title: '本当にあった笑える話【無料連載版】',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61XtF2XgZxL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00DW4ZYBG?ref_=msw_best_freehome_8&storeType=ebooks',
title: '宝石の国(1) (アフタヌーンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61FXZ-+RZLL._BG0,0,0,0_FMpng_SX300_.jpg' }]

/*
* Model
* sessionの間値を維持しておくための仕組み
*/
function initData(app) {
let data = app.data;
if(!data.books) {
data.books = {
data: [],
page: 0
}
}

if(!data.status) {
data.status = Status.welcome
}

return data;
};


/*
* Intents
* API.AI 呼び出されるIntent
*/
function showBooksIntent(app) {
console.log('called showBooksIntent action')
initData(app)
app.data.status = Status.showBooks
if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) {
console.log('*** has screen ***')
let response = '本日のおすすめの無料コミックはこちらです。'
const items = books.map((book) => {
const item = app.buildOptionItem(book.title, book.title)
item.setTitle(book.title)
item.setImage(book.image, book.title)
return item
})

const carousel = app.buildCarousel().addItems(items)
app.data.books.data = books
app.askWithCarousel(response, carousel)
return

} else {
console.log('*** do not has screen ***')
const offset = i * 3
if(offset + 3 > books.length) return app.tell('申し訳ございません。おすすめコミックが見つかりませんでした。')
let response = '本日無料のおすすめコミックは「' + books[offset] + '」「' + books[offset + 1] + '」「' + books[offset+ 2] + '」'
response += '他のコミックを探しますか?'
app.ask(response);
return
}
}

function confirmIntent(app) {
console.log('called showMoreIntent action')
initData(app)

if(app.data.status == Status.showBooks) {
app.data.books.page += 1
showBooksIntent(app)
return
} else {
app.tell('申し訳ございません。よくわかりませんでした。');
}
}

function selectedBookIntent(app) {
console.log('called selectedBookIntent action')
initData(app)
app.data.status = Status.SELECTED_BOOK

const book = app.data.books.data.find(book => {
return book.title == app.getSelectedOption()
})

if(!book) return app.tell('申し訳ございません。通信に失敗致しました。しばらくしてからもう一度御利用ください。');

const response = app.buildRichResponse()
response.addSimpleResponse(book.title + 'ですね')
const card = app.buildBasicCard(book.title)
.setTitle(book.title)
.setImage(book.image, book.title)
.addButton('コミックを入手する', book.url)

response.addBasicCard(card)
app.tell(response)
}

/*
* Mapping
* 実際に cloud functions に公開するメソッド
* API.AIからwebhookでこのメソッドが毎回呼ばれる
* Actions on Google SDK を使ってactions名とIntentメソッドの紐付けを行う
*/
const Functions = functions.https.onRequest((request, response) => {
const app = new App({request, response});
console.log('Request headers: ' + JSON.stringify(request.headers));
console.log('Request body: ' + JSON.stringify(request.body));

const actionMap = new Map();
actionMap.set(Actions.WELCOME, showBooksIntent);
actionMap.set(Actions.SHOW_BOOKS, showBooksIntent);
actionMap.set(Actions.CONFIRM, confirmIntent)
actionMap.set(Actions.SELECTED_BOOK, selectedBookIntent)
app.handleRequest(actionMap);
})

module.exports = {
Functions
}

デプロイとAPI.AIとの紐付け

ここを参照するといい https://developers.google.com/actions/apiai/deploy-fulfillment

以下のことをやる

$ npm install -g firebase-tools
$ firebase login
$ firebase init
$ npm install
$ firebase deploy --only functions

deployが終わると console に

Function URL (Functions): https://hogehogehogehoge

みたいのが出てくるのでこれをコピーしておく

API.AIに戻り、 左端のFulfillmentをクリックしてDisabledになっている項目をEnabledにする そうするとwebhookを設定できるのでURLに先ほどの FUNCTION URL を入力する

Actionの登録

先ほどコード上でこのように定義したActionを実際にAPI.AIにも定義して、API.AIとコードを紐づける

const Actions = {
WELCOME: 'welcome',
SHOW_BOOKS: 'show.books',
CONFIRM: 'confirm',
SELECTED_BOOK: 'selected.book',
}

API.AI左端の intents の+ボタンを押して、intentを新規作成 その後はuser says, action, を設定する

user says には 「おすすめのコミックを教えて」 action には selected.book を記入した

スクリーンショット 2017-10-08 23.05.09.png

また、一番下に fulfillment -> use webhook というチェックボックスがあるのでチェックを入れる

これでIntentの定義完了

おなじようにSHOW_BOOKSのintentも作る さっきと同じように設定して行くのだが、唯一違うのはEventsにactions_intent_OPTIONを設定する点 これをするとリストなどの要素から選択されたときにそのインテントが呼ばれるようになる これが設定し終わったら完成

テスト

左の integrations -> google assistant を選択し、upload draft をする

スクリーンショット 2017-10-08 23.07.27.png

そうすると actions on google の console に飛ばされて、アプリの情報の入力を迫られる

とりあえず全部埋めておく

infomationの入力が終わったら左のタブのsimulatorという項目を選ぶ

あとは試す

こんなかんじになる 

https://photos.app.goo.gl/w4ckVZVEH00cXRaO2

Google Play とかと違ってデベロッパーアカウントを購入する必要はなさそう とりあえずできたぜ。リリースもしてみたぜ。

審査通るといいな

Actions-on-Google SDK を使う時にApiAiAppが読み込めない問題が起きた時の対処法

GoogleAssistant app を作るために node で actionsdkを読み込んだときに起きた問題

undefined:1
undefined
^

SyntaxError: Unexpected token u in JSON at position 0
at JSON.parse ()
at ApiAiApp.AssistantApp (/data/app/node_modules/actions-on-google/assistant-app.js:128:23)
at ApiAiApp (/data/app/node_modules/actions-on-google/api-ai-app.js:78:5)
at Server. (/data/app/app.js:16:13)
at Server. (/data/app/node_modules/engine.io/lib/server.js:472:22)
at Server. (/data/app/node_modules/socket.io/lib/index.js:307:16)
at emitTwo (events.js:125:13)
at Server.emit (events.js:213:7)
at parserOnIncoming (_http_server.js:602:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:116:23)

よくわからんけどこんな感じのエラーが起きる ライブラリの内部で使っているパーサー周りでなんか問題があるっぽい

github.com

調べて見たらbody-parserを使ったらうまくいったよって書いてあったのでその通りにしたらうまく行った

const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());