タニタ体組成計(Health Planet)のデータをFitbitに反映させる

IT

日々の身体の状態管理としてFitbitを利用しています。Fitbitには体重と体脂肪率の管理もできるのですが、入力は手動でやる必要があります。体重などはできれば毎日管理したいのですが、毎回手入力となると中々めんどうです
そこで、タニタの体組成計を利用することで、Fitbitの体重と体脂肪率の入力を自動で行いたいと思います。自動化のために必要となるWebAPIの実行には、Pythonを利用しています。

APIの利用をしたことがほぼなく、OAuth2もなにぞやってところから始めたので、分かりにくい点も多々あるかと思います。やさしい目で読んでいただければと思います。

タニタ体組成計(Health Planet)からデータを取得

タニタ体組成計のデータは、タニタが提供しているHealth Planet APIを利用することで取得できます。こちらのAPIを使用するためには、OAuth 2.0による認証が必要になります。この認証を行うための事前準備として、Health Planetの登録画面にてアプリケーション登録を行い下記2つの情報を入手する必要があります。

  • client_id
  • client_secret

これら2つの情報は、OAuth2.0認証を利用するためのOAuth2.0クライアントクレデンシャル(アプリの情報)です。クレデンシャルは タニタアプリ開発者がOAuth認証を行う資格があることを示すものです。
これらのキーは、Health Planet ヘルスプラネットにてログインを行った後の画面で取得できます。(会員登録をされていない方は、会員登録しておくこと)

Health Planet APIを利用するためのアプリ登録

Health Planet ヘルスプラネットにアクセスする

ログインをクリックしてログインします

右上の登録情報の確認・変更をクリックします

項目のサービス連携をクリックします

アプリ連携のアプリケーション開発者の方はこちらをクリックします

新規登録をクリックします

必要事項を入力して、規約に同意して登録をクリックします

今回はWebアプリやスマホアプリではなくpythonからアクセスするためURL、ドメインは適当な値を指定します(変更が必要になった場合は後から修正できます)。

  • サービス名: 任意
  • ウェブサイト: 適当なURL
  • ホストドメイン: 適当なドメイン
  • メールアドレス: 自身のアドレス
  • 説明: 任意
  • アプリケーションタイプ: Webアプリケーション

Client IDとClient secretを確認して、新規登録をクリックします

ユーザに(Webブラウザ経由で)アクセス許可を求める(OAuth 2.0認証)

Health Planet API 仕様書によると、/oauth/auth メソッドでOauth認証/oauth/token メソッドでトークンの取得ができるようなので、こちらを実行していきます。

OAuth認証をブラウザに表示するプログラム

pythonでOAuth認証メソッドを実行し、ローカルwebサーバーで表示します。
/oauth/auth メソッドを実行するコードは下記になります。次から要点を説明していきます。

import requests
import http.server
import socketserver

CLIENT_ID = 'XXXX.XXXXXXXXXX.apps.healthplanet.jp'
REDIRECT_URL = 'http://localhost:8080'
SCOPE = 'innerscan'
RESPONSE_TYPE = 'code'

payload = {'client_id': CLIENT_ID, 'redirect_uri': 'https://www.healthplanet.jp/success.html', 'scope': SCOPE, 'response_type': RESPONSE_TYPE}
get_url_info = requests.get('https://www.healthplanet.jp/oauth/auth', params=payload)

get_url_info.encoding = get_url_info.apparent_encoding # エンコード情報を適切に判定しないので取得
f = open('index.html', 'w', encoding=get_url_info.encoding)
f.write(get_url_info.text)
f.close()

PORT = 8000
Handler = http.server.SimpleHTTPRequestHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
    print("serving at port", PORT)
    httpd.serve_forever()

まず、GETリクエストのためにRequestsライブラリを使用しているので、インストールしておきます。

pip install requests
CLIENT_ID = 'XXXX.XXXXXXXXXX.apps.healthplanet.jp'
REDIRECT_URL = 'http://localhost:8080'
SCOPE = 'innerscan'
RESPONSE_TYPE = 'code'

CLIENT_ID は、先ほど新規登録したアプリのClient IDを入力して下さい。
SCOPE は、体重と体脂肪率を取得したいのでinnerscanとしています。他の情報(血圧情報など)も取得したい場合は、仕様書を参考にカンマ区切りで追加して下さい。

get_url_info.encoding = get_url_info.apparent_encoding # エンコード情報を適切に判定しないので取得
f = open('index.html', 'w', encoding=get_url_info.encoding)

リクエストのレスポンスをそのままhtmlファイルに書き込むと、エンコード情報が適切に判断されずに文字化けしてしまいます。
apparent_encoding を使うといいようなので、書き込みの際に取得した正しいエンコードを指定します。
カレントディレクトリにindex.htmlファイルを作成するので、任意のディレクトリに移動してから実行して下さい。

PORT = 8080
Handler = http.server.SimpleHTTPRequestHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
    print("serving at port", PORT)
    httpd.serve_forever()

socketserverでローカルwebサーバーを立ち上げ、index.htmlをブラウザに表示します。

ブラウザで http://localhost:8080/ にアクセス

ログインIDとパスワードを入力して、submitをクリック

アクセスを許可をクリック

アクセス許可をクリック後にトークンの取得を行うのですが、こちらはアクセスを許可してから10分以内に完了させる必要があります。アクセス許可をする前に、トークンの取得処理を行えるようにしておくことをおすすめします。

アドレスのコードをコピー

アドレスにcodeというパラメータが付与されています。こちらを次のトークンを取得するときに使用するのでコピーなどしておきます。
codeが取得できたら、ローカルサーバーを停止してしまって大丈夫です。

トークンを取得する

トークンの取得は、アプリのアクセス許可を完了してから10分以内に完了させる必要があります。
/oauth/token メソッドを実行するコードは下記になります。

import requests
import pprint

CLIENT_ID = 'XXXX.XXXXXXXXXX.apps.healthplanet.jp'
CLIENT_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
CODE = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
REDIRECT_URL = 'https://www.healthplanet.jp/success.html'

payload = {'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET, 'redirect_uri': 'https://www.healthplanet.jp/success.html', 'code': CODE, 'grant_type': 'authorization_code'}
r = requests.post('https://www.healthplanet.jp/oauth/token.', params=payload)

if 'json' in r.headers.get('content-type'):
    result = r.json()
    pprint.pprint(result)
else:
    result = r.text
    print(result)

下記変数を、先ほどコピーしておいたcodeと、ご自身のClient IDとClient secretに書き換えて下さい。

CLIENT_ID = 'XXXX.XXXXXXXXXX.apps.healthplanet.jp'
CLIENT_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
CODE = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

プログラムが正しく実行できていれば下記のようなレスポンスが取得できます。

{'access_token': 'XXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
 'expires_in': 2592000,
 'refresh_token': 'XXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'}

体組成計のデータを取得

/status/innerscan メソッドを使用して体重と体脂肪率のデータを取得してみます。
参考までに、二日前までのデータを取得して表示するプログラムになります。

import requests
import pprint
import datetime

ACCESS_TOKEN = 'XXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
REFRESH_TOKEN = 'XXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
TAG = '6021,6022' # 6021:体重 (kg) / 6022:体脂肪率 (%)
DATE = '1' # from to の日付タイプを指定する。0:登録日付 / 1:測定日付
TO = '' # 取得期間 to を指定する。yyyyMMddHHmmss 形式で指定し、必ず from < to でなければならない。未指定の場合は現時刻が指定される

today = datetime.datetime.now()
yesterday = today - datetime.timedelta(days=2)
yesterday = yesterday.strftime("%Y%m%d%H%M%S") #yyyyMMddHHmmss形式に変換

payload = {'access_token': ACCESS_TOKEN, 'tag': TAG, 'date': DATE, 'from': yesterday, 'to': TO}
r = requests.post('https://www.healthplanet.jp/status/innerscan.json', params=payload)

if 'json' in r.headers.get('content-type'):
    result = r.json()
    pprint.pprint(result)
else:
    result = r.text
    print(result)

取得に成功すると下記のようなjsonレスポンスを取得できます。

{'birth_date': 'XXXXXXXX',
 'data': [{'date': '202104160717',
           'keydata': '80.50',
           'model': '01000145',
           'tag': '6021'},
          {'date': '202104160717',
           'keydata': '19.40',
           'model': '01000145',
           'tag': '6022'},
          {'date': '202104150714',
           'keydata': '80.70',
           'model': '01000145',
           'tag': '6021'},
          {'date': '202104150714',
           'keydata': '20.60',
           'model': '01000145',
           'tag': '6022'}],
 'height': '176',
 'sex': 'male'}

Fitbitへの反映

Fitbit APIを利用するためのアプリ登録

fitbitの開発サイトを表示してLog inをクリック

赤色のLog inをクリック

ログイン後、Manage > Register An App をクリック

必要項目を入力し、Registerをクリックしてアプリの登録を行う

試すだけなので、URLはローカルホストにします。
注意点は、

  • Desctiptionは10文字以上
  • OAuth 2.0 Application TypeにはClientまたはPersonalを選択
  • Callback URLにはリダイレクトが発生しないURLを入力(http://localhost/)

OAuth 2.0 Client IDとClient Secret をメモしておきます

アプリへのOAuth 2.0認証を行う

OAuth 2.0 tutorial pageをクリックして認証のページを表示する

チュートリアルページで認証を進めていきます

ある程度必要な情報は自動入力されています。

  • Flow typeにImplicit Grant Flowを選択
  • scopeに含めたくないものがある場合はチェックを外す

入力欄の下にある長いリンクをクリック

許可をクリックして、アプリを許可します

許可後に表示されるページのURLにaccess_tokenが含まれているので、URLをコピーします

URLを全てコピーしてしまって大丈夫です

認証ページの2: Parse responseの入力欄に貼り付けます

説明文には、#scope以降を貼り付けるように書いてありますが、#scopeはなく、URLをそのまま貼り付けても解析されるのでそのまま貼り付けます

取得のテストを行う

Make Requestにプロフィール情報を取得するcurlコマンドが表示されているので、ローカルのターミナル上にペーストしてcurlを叩きます。

レスポンスにHTTPステータスコード HTTP/2 200 が返っていれば成功です

$ curl -i -H "Authorization: Bearer <OAuth 2.0 Access Token>
>  https://api.fitbit.com/1/user/-/profile.json
HTTP/2 200 
date: Wed, 17 Mar 2021 13:02:19 GMT
content-type: application/json; charset=utf-8
fitbit-rate-limit-limit: 150
fitbit-rate-limit-remaining: 150
fitbit-rate-limit-reset: 3461
x-frame-options: SAMEORIGIN
via: 1.1 google
cf-cache-status: DYNAMIC
cf-request-id: 08e1e0b79f00000a748c17d000000001
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 63166a38fbec0a74-KIX

{"user":
(以下略)

レスポンスに下記のようなaccess_tokenやrefresh_tokenの情報があります。こちらは後々利用するのでコピーしてtxtファイルに保存しておいて下さい。

{'access_token': 'XXXXX', 'expires_in': 28800, 'refresh_token': 'XXXXX', 'scope': ['settings', 'activity', 'nutrition', 'heartrate', 'weight', 'social', 'sleep', 'profile', 'location'], 'token_type': 'Bearer', 'user_id': 'XXXXXX', 'expires_at': 1616234469.723309}

タニタ体組成計データをFitbitに反映させる

Health Planet APIで取得した体重と体脂肪率をFitbitに反映させるプログラム

その日に測定されたデータ(日付、体重、体脂肪率)を反映するものになります。
体組成計で測定した後に、こちらのプログラムを実行させるとFibitに反映されます。
私は、ラズパイで毎日日付変更前に定期実行されるようにしています。

import sys
import datetime
from ast import literal_eval
import requests
import fitbit

TANITA_ACCESS_TOKEN = 'XXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
TANITA_REFRESH_TOKEN = 'XXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
TANITA_TAG = '6021,6022' # 6021:体重 (kg) / 6022:体脂肪率 (%)
TANITA_DATE = '1' # from to 日付タイプ。0:登録日付 / 1:測定日付
TANITA_TO = '' # 取得期間。 yyyyMMddHHmmss 形式。未指定の場合は現時刻が指定される

FITBIT_CLIENT_ID     = "XXXXXX"
FITBIT_CLIENT_SECRET = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
FITBIT_TOKEN_FILE    = "fitbit_token.txt"

def getBodyData(data):
    date_from = data.get('date').strftime("%Y%m%d%H%M%S") #yyyyMMddHHmmss形式に変換(時間は000000)

    payload = {'access_token': TANITA_ACCESS_TOKEN, 'tag': TANITA_TAG, 'date': TANITA_DATE, 'from': date_from, 'to': TANITA_TO}
    r = requests.post('https://www.healthplanet.jp/status/innerscan.json', params=payload)

    if 'json' in r.headers.get('content-type'):
        result = r.json()

        if not result['data']:
            print("date:", data.get('date'),', there is no data')
            sys.exit()

        weight_latest = result['data'][0]["keydata"]
        fat_latest = result['data'][1]["keydata"]

        data['weight'] = weight_latest
        data['fat'] = fat_latest
    else:
        result = r.text
        print(result)

def updateFitbitToken(token):
    f = open(FITBIT_TOKEN_FILE, 'w')
    f.write(str(token))
    f.close()
    return

def pound_to_kg(pound):
    kg = pound * 0.454
    return kg

def kg_to_pound(kg):
    pound = float(kg) * 2.2046
    pound = round(pound, 3)
    return pound

def uploadBodyData(data):
    tokens = open(FITBIT_TOKEN_FILE).read()
    token_dict = literal_eval(tokens)
    access_token = token_dict['access_token']
    refresh_token = token_dict['refresh_token']

    client = fitbit.Fitbit(FITBIT_CLIENT_ID, FITBIT_CLIENT_SECRET,
        access_token = access_token, refresh_token = refresh_token, refresh_cb = updateFitbitToken)

    date = data.get('date')
    weight = data.get('weight')
    fat = data.get('fat')

    client.body(date=date, data={"weight": kg_to_pound(weight), "fat": fat})
    print("upload")
    print("date:", date, ", weight:", weight, ", fat:", fat)


today = datetime.datetime.now().date()
bodydata = {'date': today}
getBodyData(bodydata)
uploadBodyData(bodydata)

要点だけ説明します。

Health Planet APIは上で説明した内容とほぼ変わらないので割愛します。
Fitbit APIの利用は、python-fitbitモジュールを利用しています。インストールをお忘れなく。

pip install fitbit

Fitbit APIが採用しているOAuth2は、一定時間を超えるとアクセストークンが失効します(デフォルトでは8時間)。
この対策として、API実行時にトークンが失効していた場合はリフレッシュするようになっています。

def uploadBodyData(data):
    tokens = open(FITBIT_TOKEN_FILE).read()
    token_dict = literal_eval(tokens)
    access_token = token_dict['access_token']
    refresh_token = token_dict['refresh_token']

    client = fitbit.Fitbit(FITBIT_CLIENT_ID, FITBIT_CLIENT_SECRET,
        access_token = access_token, refresh_token = refresh_token, refresh_cb = updateFitbitToken)

こちらの記事を参考にさせていただきました。

PythonからFitbit APIを使ってデータを取得する(OAuth2)

トークン情報は下記ファイルに保存しています。事前に作成しておきましょう。

FITBIT_TOKEN_FILE    = "fitbit_token.txt"

こちらのファイルには、上で説明したcurlコマンドのレスポンスに含まれているaccess tokenやrefresh tokenの情報を保存しています。

あとは細かい点ですが、python-fitbitモジュールの体重はポンドで指定する必要があるので、kgからポンドへ変更しています。

    client.body(date=date, data={"weight": kg_to_pound(weight), "fat": fat})

まとめ

OAuth認証の理解がなかったため、データの取得よりも認証の方が苦労しました。特にHealth Planet APIの方が大変でした。Fitbit APIの方は手順通りやれば簡単にできるようになっていたのでとてもありがたかったです。
今後はもっと他のAPIも利用していきたいです。

参考にさせていただいた記事

API

Posted by noramyao3