Google Home から音声で Google Fit にアクティビティを追加する2(nodejs・Google Fit 編)
この記事は、Google Home から音声で Google Fit にアクティビティを追加する1(Firebase・IFTTT 編) のつづきです。
node.js アプリを作成し、Firebase の読み取り、Google Fit へのデータ登録を行っていきます。
抜粋で説明しているため、ソース全体はGitHubを参照してください。
URL 設計
Google Fit API を利用してユーザデータを登録するためには、Google で OAuth2 認証をする必要があります。
このため、ユーザにはアプリ起動後、web 画面を開いて OAuth2 認証してもらうことにしました。
1回認証してもらった後は、トークン期限切れになっても、Google Fit API 実行時に裏でリフレッシュトークン認証をしてトークンを再発行することで、ユーザのアクションは不要としています。
パス | 処理 |
---|---|
/ | アプリホームページ Google ログイン済みでない場合はログインリンクを表示 |
/auth/google | Google OAuth2 画面へリダイレクト |
/auth/google/callback | Google OAuth2 のコールバック URL トークンをメモリに保存して / にリダイレクト |
Firebase アプリの秘密鍵の生成
アプリに Firebase を追加する の手順に従い、Firebase に admin 権限でアクセスするための秘密鍵を生成します。
Firebase の読み込み
web 画面が必要となるため、 今回は express でアプリを作成しました。
expresss 初期化
$ mkdir google-assistant-hook $ cd google-assistant-hook $ express --git --hbs . $ npm i
Firebase Admin SDK の追加
$ npm install firebase-admin --save
Firebase のデータ読み込み
app.js の末尾に以下のコードを追加します。
var admin = require('firebase-admin'); // Firebase アプリの秘密鍵の生成でダウンロードした JSON ファイル var serviceAccount = require('../config/serviceAccountKey.json'); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: `https://refined-byte-XXXXXX.firebaseio.com` }); var db = admin.database(); var ref = db.ref('google-fit/log'); ref.on('child_added', function (snapshot, prevChildKey) { console.log(prevChildKey, snapshot.val()); // 読み終わったデータの削除 var logRef = ref.child(prevChildKey); logRef.set(null); }, function (err) { console.log('Failed to read data', err); });
Firebase の google-fit/log にデータが追加されるとイベントフックが呼び出されてコンソールに値が表示されます。
ログデータは、読み終わったら不要となるため、データを Firebase 上に溜め込まないためにもその場で削除しています。
Google Fit のアクティビティデータの登録方法
Google Fit API を利用することで、Google Fit にアクティビティのデータを登録することができます。
データの登録は、以下の流れで行います。
- アプリでデータを登録する領域( Data Sources )を作成します(初回のみ)
- Data Sources に アクティビティデータ( Data Sets )を登録します
Google Fit API の詳細については、Google Fit API のドキュメント を参照してください。
curl サンプル
TOKEN は Google OAuth 2.0 Playground で作成することができます。
ただし、Google OAuth 2.0 Playground のトークンを利用して作成した Data Sources には、OAuth2 認証したアプリから Data Sets を登録できないので、curl で試す場合には、アプリとは別の名前で登録してください。
Data Source 登録
curl --header "Authorization: Bearer $TOKEN" -X POST \ --header "Content-Type: application/json;encoding=utf-8" \ "https://www.googleapis.com/fitness/v1/users/me/dataSources" \ -d '{ "device": { "uid": "100000000", "type": "phone", "version": "", "model": "SO-02H", "manufacturer": "Sony" }, "dataType": { "name": "com.google.activity.segment", "field": [ { "format": "integer", "name": "activity" } ] }, "application": { "version": "1", "name": "Log Google Home", "detailsUrl": "http:\/\/example.com" }, "type": "derived", "dataStreamName": "LogGoogleHome" }' -i
Data Sets 追加
https://www.googleapis.com/fitness/v1/users/me/dataSources/dataStreamId/datasets/startTimeNanos-endTimeNanos
パラメータ | 値 |
---|---|
dataStreamId | Data Source 登録のレスポンスに含まれる dataStreamId を URL エンコードした値 |
startTimeNanos | アクティビティ開始時刻の UnixTime (ナノ秒) |
endTimeNanos | アクティビティ終了時刻の UnixTime (ナノ秒) |
curl -XPATCH "https://www.googleapis.com/fitness/v1/users/me/dataSources/derived%3Acom.google.activity.segment%3A407408718192%3ASony%3ASO-02H%3A16062204%3ALogGoogleHome/datasets/1513112400000000000-1513116000000000000" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -d '{ "dataSourceId": "derived:com.google.activity.segment:407408718192:Sony:SO-02H:16062204:LogGoogleHome", "maxEndTimeNs": "1513116000000000000", "minStartTimeNs": "1513112400000000000", "point": [ { "dataTypeName": "com.google.activity.segment", "endTimeNanos": "1513116000000000000", "startTimeNanos": "1513112400000000000", "value": [ { "intVal": 21, "mapVal": [] } ] } ] }' -i
Google Fit へのデータ登録
Google APIs Node.js Client
Google Fit の API 呼び出しには、Google APIs Node.js Clientを利用しました。
このライブラリはまだ、アルファ版なので遊び以外の目的で利用する場合はご注意ください。
npm install googleapis --save
OAuth2 用の設定
Google Fit にアクティビティデータを登録するには、OAuth2 のトークンが必要なので、まずは OAuth2 認証処理を実装します。
Google API Console の 認証情報
を開き、 OAuth 2.0 クライアント ID
にアプリケーションを追加します。
今回は、Firebase が作成したところを編集して以下を定義しました。
- 承認済みの JavaScript 生成元
- 承認済みのリダイレクト URI
上記を定義後、ページ上部のリンクから JSON をダウンロードして express プロジェクトの config 配下に googleClientSecret.json
という名前で配置しておきます。
OAuth2 認証
トークン管理用のクラス GoogleOAuthClient を作成します。
本アプリを利用するのは1人の前提なので、認証済みのクレデンシャル情報はシングルトンとしてサーバメモリ上で保持します。
Google APIs Node.js Client の OAuth2 Client を生成し、メンバ変数として保持しています。
var google = require('googleapis'); var clientKey = require('../config/googleClientSecret.json'); var OAuth2 = google.auth.OAuth2; class GoogleOAuthClient { constructor() { this.client = new OAuth2( clientKey.web.client_id, clientKey.web.client_secret, clientKey.web.redirect_uris[1] ); } getClient() { return this.client; } isAuthenticated() { return Boolean(this.client.credentials && this.client.credentials.expiry_date); } } module.exports = new GoogleOAuthClient();
/auth/google
へアクセスがあった場合に OAuth2 画面を表示し、OAuth2 callback 時にクレデンシャルを保存します。
app.use('/auth', require('./routes/auth'));
var express = require('express'); var GoogleOAuthClient = require('../lib/GoogleOAuthClient'); var router = express.Router(); var oauth2Client = GoogleOAuthClient.getClient(); var scopes = [ 'https://www.googleapis.com/auth/fitness.activity.write', 'https://www.googleapis.com/auth/fitness.blood_glucose.write', 'https://www.googleapis.com/auth/fitness.blood_pressure.write', 'https://www.googleapis.com/auth/fitness.body.write', 'https://www.googleapis.com/auth/fitness.body_temperature.write', 'https://www.googleapis.com/auth/fitness.location.write', 'https://www.googleapis.com/auth/fitness.nutrition.write', 'https://www.googleapis.com/auth/fitness.oxygen_saturation.write', 'https://www.googleapis.com/auth/fitness.reproductive_health.write' ]; var oauthUrl = oauth2Client.generateAuthUrl({ access_type: 'offline', // refresh_token を発行 prompt: 'consent', // 毎回認証画面を出す(毎回 refresh_token を発行するため) scope: scopes }); router.get('/google', function (req, res) { res.redirect(oauthUrl); }); router.get('/google/callback', function (req, res, next) { // トークン取得 // トークンが有効期限切れの場合は自動的にライブラリ内でリフレッシュトークンを取り直してくれる oauth2Client.getToken(req.query.code, function (err, tokens) { if (err) { console.error('Failed to get token.'); return next(err); } oauth2Client.credentials = tokens; console.log(oauth2Client.credentials); res.redirect('/'); }); }); module.exports = router;
npm start
でアプリを起動し、ブラウザで http://localhost:3000/auth/google にアクセスし、コンソール表示を確認します。
アクセストークンが表示されれば成功です。
Google Fit Data Source の作成
上記で取得したアクセストークンを利用して、Data Source 登録 の curl を実行し、アプリ用の Data Source を作成します。
Google Fit への登録
GoogleFit アクセス用のクラスを作成します。
var google = require('googleapis'); var GoogleOAuthClient = require('../lib/GoogleOAuthClient'); var config = require('../config/app.json'); var oauth2Client = GoogleOAuthClient.getClient(); var fitness = google.fitness('v1'); class GoogleFit { regist(data) { switch (data.text) { case '開始': this.start = new Date().getTime() * 1000000; break; case '終了': if (!this.start) { // 開始済みでない場合は無視 break; } this.end = new Date().getTime() * 1000000; if (!GoogleOAuthClient.isAuthenticated()) { console.error('Failed to get token. Please authorize application before at http://localhost:3000.'); break; } this.createDataSets(); break; default: // 開始・終了以外のデータの場合何もしない break; } } createDataSets() { return new Promise((resolve, reject) => { var params = { auth: oauth2Client, userId: 'me', dataSourceId: config.googleFit.dataSourceId, datasetId: `${this.start}-${this.end}`, resource: { 'dataSourceId': config.googleFit.dataSourceId, 'maxEndTimeNs': this.end, 'minStartTimeNs': this.start, 'point': [{ 'dataTypeName': config.googleFit.dataType, 'endTimeNanos': this.end, 'startTimeNanos': this.start, 'value': [{ 'intVal': 21, // 健康体操 'mapVal': [] }] }] } }; fitness.users.dataSources.datasets.patch(params, (err) => { // 開始・終了時間を初期化 this.start = undefined; this.end = undefined; if (err) { console.error('Failed to regist dataset to Google Fit.', err); return reject(err); } return resolve(); }); }); } } module.exports = GoogleFit;
Firebase 読み込み処理のところに Google Fit 登録の呼び出しを追加します。
var ref = db.ref(config.database.path.googleFitLog); ref.on('child_added', function (snapshot, prevChildKey) { console.log(prevChildKey, snapshot.val()); if (!prevChildKey) { // 何故か1つ目のデータのキーが null となってしまうため、ゴミデータとして無視する return; } // 非同期でデータ登録 googleFit.regist(snapshot.val()); // 読み終わったデータの削除 var logRef = ref.child(prevChildKey); logRef.set(null); }, function (err) { console.log('Failed to read data', err); });
動作確認
以上で実装は終了です。
npm start
でアプリを起動します。
Google Home に「OK グーグル!健康体操を開始して!」とつぶやいてみましょう。さらに10分後くらいに「OK グーグル!健康体操を終了して!」とつぶやいてください。
Google Fit アプリで健康体操の記録が追加されていれば成功です。
※あまり時間が短いとアプリで記録の表示が省略されてしまう場合があるので、10分程度開始から終了の間隔を置いてみてください。
感想
IFTTT に webhook 機能があることは知っていましたが、利用するには外部公開用のサーバをたてるしかないと考えていました。
なので、Firebase のリアルタイム DB を利用することで、無料で IFTTT とホームネットワーク内にたてたサーバとを連携することができて驚きました。
今は適当な Linux PC にアプリをインストールして動かしていますが、次は Raspberry pie を購入して赤外線モジュールをつなげて、エアコン操作・・・なんてことに挑戦してみたいなと思っています。