Withings Body Cardio で全世界に体重を晒す

# [Withings Body Cardio](http://amzn.to/2gCfPcv) とは？
いわゆるスマート体重計です。WiFi や Bluetooth 経由でアプリ連携するものはいくつかありますが、Withings Body Cardio はなんと Web API が用意されていて OAuth 認証でセキュアにデータを取得できるという特徴があります。お値段はちょっと高いですが API が用意されているとなれば [ヘルシープログラマ](http://amzn.to/2gCloYi) としては買わないわけにはいかないでしょう。

![body-3-carrousel.png](https://qiita-image-store.s3.amazonaws.com/0/6102/5b1a032f-2f61-cbd5-9f83-4d4d56827d5c.png)

■ Withings Body Cardio
http://www.withings.com/jp/ja/products/body-cardio

■ Amazon | Withings スマート体重計 Body Cardio ブラック Wi-Fi/Bluetooth対応【日本正規代理店品】
http://amzn.to/2gCfPcv


ちなみに Withings はフランスのデジタルヘルス家電メーカー名で、スマート体重計の製品名ではありません。スマート体重計は Body と Body Cardio の2つのラインナップがあり、今回は上位機種の Body Cardio の方で話を進めます。知らなかったのですが、Withings はノキア傘下だったんですね。

# やりたいこと
Withings API を使って、日々の計測データを取得し、グラフで表示します。
今回は Web アプリということで PHP で API を叩き、Google Charts を使ってブラウザ上にグラフを出したいと思います。
尚、タイトルに全世界に晒すと書きましたが、OAuth 認証が入るので見えるのは自分だけです。（ただしこの投稿のスクショで私の体重は晒されていますが！）

# 専用スマホアプリ
体重計のデータは計測後即座に WiFi 経由で Withings 社のサーバに保存されます。専用アプリはそれらを取得してグラフなどを表示しています。恐らく後述の Withings API と同じものを使っているのかと。ちょっと翻訳が変ですが UI も今風ですし、わりと使いやすいアプリだと思います。
<img width="414" alt="IMG_0924.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/6102/13d19703-27f2-8349-328b-8cfcc28cc0d2.png">

# では実践
使いやすいアプリがあるならわざわざ API 叩く必要無いじゃんって話になりますが、そこはやはり開発者魂というか、やっぱり bot とか slack 連携とかしたいじゃないですか。ってことで強引に進めます。

とりあえず API リファレンス。
http://oauth.withings.com/api

## まずはアプリ登録
他の OAuth サービスと同様に、まずはアプリの登録が必要です。これは特に審査は無いので下記 URL でログインし、アプリ名やアイコンなどを登録して API Key と API Secret を取得しましょう。ちなみに登録は1アカウントで1アプリだけのようです。なんか残念。

http://oauth.withings.com/


## php-withings
API は OAuth 1 での認証が必要です。今回この辺は自前で作らずライブラリを使用します。packagist で調べるといくつか見つかりましたが、一番スターの多い php-withings というものを使用したいと思います。

■ php-withings | Withings Body Metric Service API
https://github.com/Zn4rK/php-withings

php-withings は Laravel の Socialite に対応していると書かれていましたがうまく動きませんでした。Laravel のサービスプロバイダも 5.3 に対応していなかったので、今回は素の Laravel からライブラリをそのまま使用します。後述の通り仕様にないポンド換算などがされていたり、最新のAPIには対応してないようですね。機会があれば Laravel 5.3対応も含めて本家に pull req 送りたいと思います。

## インストール
composer でパッケージを追加。

```bash
$ composer require paxx/withings
```

以上。

## 各種設定
Laravel の作法に従いルーティングと APIKey などを設定。
今回は下記のような URL でアクセスします。

| URL | ページ |
|:--|:--|
| http://localhost:8000/wighings/auth  | 認証開始 |
| http://localhost:8000/wighings/callback  | リダイレクトURL |
| http://localhost:8000/wighings/graph  | グラフ表示 |

- URL ルーティング設定ファイルに追加。

```php:routes/web.php
Route::get('withings/auth',     'WithingsController@redirectToProvider');
Route::get('withings/callback', 'WithingsController@handleProviderCallback');
Route::get('withings/graph',    'WithingsController@showGraph');
```

- サービス設定ファイルに API Key, API Secret, リダイレクト URL を記述。

```php:configs/service.php
return [
        :
        :
    'withings' => [
        'client_id'     => 'API Key',
        'client_secret' => 'API Secret',
        'redirect'      => 'http://localhost:8000/withings/callback',
    ],
];
```

## コントローラー設置
次にコントローラーを作成します。URL に割り当てられているメソッドの中でゴニョゴニョしています。詳細はソース内コメントにて。

```php:app/Http/Controllers/WithingsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Paxx\Withings\Api;
use Paxx\Withings\Provider\Withings;


class WithingsController extends Controller
{
    protected $withingsConfig;

    public function __construct()
    {
        // Withings API 設定変数
        $this->withingsConfig = [
            'identifier'   => \Config::get('services.withings.client_id'),
            'secret'       => \Config::get('services.withings.client_secret'),
            'callback_uri' => \Config::get('services.withings.redirect'),
        ];
    }

    /**
     * 認証開始 /withings/auth
     */
    public function redirectToProvider()
    {
        $server = new Withings($this->withingsConfig);

        // テンポラリクレデンシャルを取得
        $temporaryCredentials = $server->getTemporaryCredentials();
        // セッションに保存
        Session::put('temporaryCredentials', serialize($temporaryCredentials));
        // 認証URLを生成
        $redirectURL = $server->getAuthorizationUrl($temporaryCredentials);

        // リダイレクト
        return redirect($redirectURL);
    }

    /**
     * リダイレクトURL /withings/callback
     */
    public function handleProviderCallback(Request $request)
    {
        $server = new Withings($this->withingsConfig);
        // テンポラリクレデンシャルと取得したトークンでアクセストークンを生成
        $temporaryCredentials = unserialize(Session::pull('temporaryCredentials'));
        $tokenCredentials = $server->getTokenCredentials($temporaryCredentials, $request->get('oauth_token'), $request->get('oauth_verifier'));
        // セッションに保存
        Session::put('access_token', serialize($tokenCredentials));
        Session::put('userid', $request->get('userid'));
        // グラフページに遷移
        return redirect('/withings/graph');
    }

    /**
     * グラフ表示 /withings/graph
     */
    public function showGraph()
    {
        // セッション復元
        $userId           = Session::get('userid');
        $tokenCredentials = unserialize(Session::get('access_token'));
        // 設定値に追加
        $this->withingsConfig = $this->withingsConfig + [
            'access_token' => $tokenCredentials->getIdentifier(),
            'token_secret' => $tokenCredentials->getSecret(),
            'user_id'      => $userId
        ];

        $api = new Api($this->withingsConfig);
        // APIから過去三ヶ月分の計測データを取得
        $results = $api->getMeasures(['startdate' => strtotime(date('Y-m-1', strtotime('-3 months'))), 'enddate' => time()]);
        // 結果を整形
        $measures = [];
        foreach ($results->groups as $group) {
            if ($weight = $group->measure->getImperial('weight')) {
                $measures[] = (object)[
                    'date'      => $group->timestamp,
                    'weight'    => $weight / 2.20462, // ※1
                ];
            }
        }
        // レンダリング
        return view('withings.graph', ['measures' => $measures]);
    }
}
```

ブラウザで http://localhost:8000/withings/auth にアクセスして、下記のような認証ページが表示されたらOK。

<img width="582" alt="スクリーンショット 2016-12-14 19.26.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/6102/b7cff8d3-356b-505f-31aa-847b013cf819.png">

認証するボタンをクリックすると先程の callback_uri にて指定した URL に oauth_token, oauth_verifier, あと userid が渡されます。それらを使ってアクセストークンを取得すると、各 API にアクセスできるようになります。

showGraph() メソッドでは API から取得したデータを整形し、Blade テンプレートにてグラフ表示するためのデータを定義しています。ソース内の ※1 にあるように、ライブラリが古いのかポンド換算となっているのでkgに戻す処理が必要でした。最初とんでもない体重のデータが取れて他人の個人情報にアクセスできちゃったのかと焦りました。

## Blade テンプレート
あとはフロントエンドだけ。今回は Google Chart にお世話になります。

■ Google Chart
https://developers.google.com/chart/

CDN があるので URL を拝借しまして、あとはグラフに表示するためのデータをループでセットしてるだけです。

```php:resources/views/withings/graph.blade.php
<html>
  <head>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
    <script type='text/javascript'>
      google.charts.load('current', {'packages':['annotationchart']});
      google.charts.setOnLoadCallback(drawChart);
      function drawChart() {
        var data = new google.visualization.DataTable();
        data.addColumn('date', 'Date');
        data.addColumn('number', '体重');

        data.addRows([
          @foreach ($measures as $measure)
            [new Date({{date('Y', $measure->date)}}, {{date('m', $measure->date) - 1}}, {{date('d', $measure->date)}}), {{$measure->weight}}],
          @endforeach
        ]);

        var chart = new google.visualization.AnnotationChart(document.getElementById('chart_div'));

        var options = {
          displayAnnotations: true, min: 60
        };

        chart.draw(data, options);
      }
    </script>
  </head>

  <body>
    <div id='chart_div' style='width: 800px; height: 400px;'></div>
  </body>
</html>
```

ブラウザからアクセスするとこの通り。

<img width="812" alt="スクリーンショット 2016-12-14 21.58.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/6102/2cd11656-50e6-0d21-fb0f-1197af0268c5.png">

実はこれを書いた前々日に今期ダイエット史上最低体重を更新したのですが、翌日忘年会で暴食しプラス1.2kg ほどリバウンドしたというオチがこのグラフで表されています。

# まとめ
Withings Body Cardio は買って正解でした。
体重以外にも体脂肪はもちろん心拍数や脈波伝播速度など日々の健康の微妙な変化にいち早く気づくためのデータを計測してくれます。
その他にも専用アプリでは計測値から健康面での指摘や、達成時にバッジが貰えたりと、ゲーム感覚で健康維持ができるのも魅力ですね。

興味ある方は是非買ってみて、体重を晒してみましょう！