毎日Learning

学んだことを共有します

GASでタニタのHealth Planet APIを叩いてスプレッドシートに出力した

友人から、タニタの体組成計をプレゼントしていただいた。

iPhoneAndroidのアプリにデータを連携できるのだが、どうもアプリのグラフだけだと見にくいし、Googleスプレッドシートにとりあえず出力しておけば、後々、いろいろできそうだしってので、ちょっとやってみた。

Health Planet APIの仕様については、 ↓

www.healthplanet.jp

以下、書いたGoogle App Scriptについて、解説する。

Health PlanetでClientを登録する

Health Planet APIは、OAuth2.0に準拠しているとのことで、最初にHealth Planet側にクライアントを登録する必要がある。

以下の画面にアクセスして、 f:id:yoshiyoshifujii:20191229134835p:plain

APIの設定をする。 f:id:yoshiyoshifujii:20191229134941p:plain

すると、 Client IDClient secret が払出される。

f:id:yoshiyoshifujii:20191229135121p:plain

これ、後で使う。

Googleスプレッドシートをつくる

こんな感じにした。

f:id:yoshiyoshifujii:20191229135424p:plain

1行目に、認証用に Auth と書いた図形と、AccessTokenを取得する用の Token と書いた図形と、体組成データを取得する用の InnerScan という図形を用意した。

普段は、 InnerScan をクリックすると、データをサーバから取得してくるのだが、AccessTokenの有効期限が、30日っぽいので、また、認証してTokenを払出してっていうのをcurlとかでやるの面倒なので、スプレッドシートから実行できるようにしてる。

以下、各ボタンに対応するGASを書いたので、1個ずつ動きもふまえて解説する。

Authボタン

f:id:yoshiyoshifujii:20191229140013p:plain

こんな感じで、モーダルが開く。

ここでは、Health Planet API/oauth/auth にアクセスして、返ってくるHTMLをモーダルに表示している。

function openOauthAuth() {
  const text = _getOauthAuthText();
  _openDialog(text);
}

GASに、こんなfunctionを作って、このfunctionをクリック時に呼び出すように指定している。

処理としては、 /oauth/auth にアクセスして、返ってきたHTMLをダイアログに渡して描画している。

/oauth/auth にアクセスする処理は、以下。

function _getOauthAuthText() {
  const url = "https://www.healthplanet.jp/oauth/auth";
  const params = {
    'method': "post",
    'payload': {
      'client_id': _getClientId(),
      'redirect_uri': "https://www.healthplanet.jp/success.html",
      'scope': "innerscan",
      'response_type': "code"
    }
  };
  const res = UrlFetchApp.fetch(url, params);
  const text = res.getContentText("shift_jis");
  return text;
}

GASの Class UrlFetchApp  |  Apps Script  |  Google Developers を使って、Health Planet APIを叩いている。

_getClientId() は、

function _getClientId() {
  return PropertiesService.getScriptProperties().getProperty("ClientId");
}

GASの Class PropertiesService  |  Apps Script  |  Google Developers を使っている。

ClientIdとかをコードに埋め込まないで、Script内で有効なPropertiesとして設定して使うようにした。 後程、ClientSecretも出てくるが、同じようにPropertiesに設定して使っている。

ダイアログを開くところは、

function _openDialog(text) {
  const html = HtmlService.createHtmlOutput(text);
  SpreadsheetApp.getUi().showModalDialog(html, "OAuth auth");
}

GASの Class Ui  |  Apps Script  |  Google Developers を使って開いている。

ダイアログを開いたら、IDとPWを入力してログインする。

アクセスを許可するか、確認するボタンが表示されるので、許可する。

f:id:yoshiyoshifujii:20191229144654p:plain

コードが表示されるので、このコードをコピーする。

f:id:yoshiyoshifujii:20191229144802p:plain

x ボタンでモーダルを閉じて、コピーしたコードを、スプレッドシートのセルB2に貼り付ける。

セルB2を、後程、Tokenボタンをクリックした際に取得して、Tokenの取得処理に使うようにしている。

Tokenボタン

Tokenボタンをクリックすると、セルB2のコードを使って、Health Plane API/oauth/token を呼び出して、Access Tokenを取得する。

function getOauthToken() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const code = sheet.getRange(1,2).getValue();
  const tokenJson = _getOauthToken(code);
  sheet.getRange(1,4).setValue(tokenJson['access_token']);
}

Access Tokenを取得したら、それをセルD2に書き込むようにした。

さらに次のInnerScanボタンをクリックしたときに使うようにしている。

/oauth/token の呼び出し処理は、以下。

function _getOauthToken(code) {
  const url = "https://www.healthplanet.jp/oauth/token";
  const params = {
    'method': "post",
    'payload': {
      'client_id': _getClientId(),
      'client_secret': _getClientSecret(),
      'redirect_uri': "https://localhost",
      'code': code,
      'grant_type': "authorization_code"
    }
  };
  return _fetchToJson(url, params);
}

ちゃんと取れたら、 JSON.parse する感じにして、取れなかったらErrorをthrowするみたいなことにしている。

InnerScanボタン

最後に、Access Tokenを使って、体組成計データを取得し、それをスプレッドシートに出力する。

function getInnerScan() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const accessToken = sheet.getRange(1,4).getValue();
  const lastRow = sheet.getLastRow();
  const lastRowDate = _getRowDate(sheet, lastRow);
  const from = lastRowDate + "00";
  const json = _getInnerScanJson(accessToken, from);
  const dateKeyDataValueMap = _convertDateKeyDataValueMap(json);
  const tagColumnList = _getTagColumnList(sheet);

  var row = lastRow;
  const dateList = Object.keys(dateKeyDataValueMap)
    .sort()
    .filter(function(date) {
      return date !== lastRowDate;
    })
    .map(function(date) {
      row++;
      sheet.getRange(row, 1).setValue(date);
      const dict = dateKeyDataValueMap[date];
      tagColumnList.forEach(function(tag) {
        sheet.getRange(row, tag.col).setValue(dict[tag.value]);
      });
      return date;
    });
  
  if (dateList.length === 0) {
    SpreadsheetApp.getUi().alert("No data");
    return;
  }
}

セルD4のAccess Tokenを取得して使う。

また、シートの最終行数を取得し、A列の最終行の値を取得している。

これは、最後に読み込んだ日付を取得して、そこから後のデータをAPIで取得するようにしたいので、そのような処理にした。

Health Planet API/status/innerscan.format が返すデータ構造は、日付が降順だったり、日付ごとではなく、フラットにデータとして返ってくるので、これをスプレッドシートの1行データに整形する必要がある。

function _convertDateKeyDataValueMap(json) {
  return json.data.reduce(function(acc, d) {
    const dict = (d.date in acc) ? acc[d.date] : {};
    dict[d.tag] = d.keydata;
    acc[d.date] = dict;
    return acc;
  }, {});
}

reduce で畳み込んで、日付をキーとしたObjectに変換した。

あと、フラットなデータ構造の中に入っているTagに対応する値を取得して、対応する列に表示していくためにもキーとなるTag情報とかがいる。

このあたりは、シートから情報を取得して、良い感じに列に収まって表示されるようにする必要があった。

function _getTagColumnList(sheet) {
  // B2:I2までを読み込む
  var startCol = 2;
  const range = sheet.getRange(2, startCol, 1, 8);
  return range.getValues()[0].map(function(v) {
    return {
      'col': startCol++,
      'value': v
    };
  });
}

畳み込んだObjectの日付キーのリストを昇順にしてループし上から取り出していくようにした。

1日ずつ取り出しつつ、Tagもループして、対応するTagの値を取得して、セルに値を入れていく感じ。

まとめ

これで、毎日、体組成計に乗って、アプリでデータ取得して、それをスプレッドシートに取得する生活ができた。

スクリプト全文は、Gistに貼り付けておいた。

HealthPlanetScript.js · GitHub

以上です。