タニタの「Health Planet API」から1週間の体重の平均値を毎朝LINE通知させる

これは、[ディップ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dip-dev)の記事になります。

## 経緯

私は趣味で筋トレやボディメイクに取り組んでいます。その一環として毎朝、タニタの体重計で体重を測っています。

タニタの体重計は、Bluetoothを介して、測定したデータを専用アプリ「Health Planet」に送信してくれる機能があります。この機能を使って、毎日の測定結果をデジタルデータとして記録しています。

https://apps.apple.com/jp/app/%E3%83%98%E3%83%AB%E3%82%B9%E3%83%97%E3%83%A9%E3%83%8D%E3%83%83%E3%83%88-%E5%81%A5%E5%BA%B7%E7%AE%A1%E7%90%86%E3%82%A2%E3%83%97%E3%83%AA/id692700901

人間の体重は、食事量や水分量、睡眠時間などで常に変動しているため、ボディメイクでは、1日ごとの体重の変化よりも、週単位、月単位での中長期的な平均の推移を見てあげることが重要です。しかし、毎日の体重を記録し、その平均を手計算するのはとても面倒くさい作業です。

そこで、タニタの「Health Planet API」の存在を知りました。このAPIを利用して、自動でデータの取得し、平均値や増減の算出を行い、毎日必ず見るLINEに通知を送れば、自身のボディメイクにかなり役に立つと思い、実装してみようと考えました。

## Health Planet API とは？

「Health Planet API」は、日時や取得項目などを指定して、測定日時や体重、体脂肪率などを取得できるAPIです。

詳しくは、Health Planet API 仕様書をご覧ください。

https://www.healthplanet.jp/apis/api.html


## 今回作ったもの

直近7日間の平均体重と先週分（8~14日前）の平均体重との増減を、毎朝8時にLINE通知するシンプルなものです。

![スクリーンショット 2023-12-10 11.25.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635876/f61df325-b28d-db80-73e3-24c3c8f9a4e4.png)

## 実装

今回は、大まかに
1. Health Planet APIから測定データ取得
1. 取得した測定データから平均値や増減の算出
1. LINE Notifyを使って通知
1. GitHub Actionsのscheduleイベントから毎朝8時に通知処理を実行

という流れで実装しました。

実装のコードは掻い摘んで載せるので、全貌はGitHubのリポジトリをご覧ください。

https://github.com/dovicie/weight-notification

### 1. Health Planet API から体重データを取得

#### その前に）Health Planet APIのユーザー認証

Health Planet APIは、誰でもすぐ叩けるというわけではなく、ユーザー認証を行う必要があります。
ユーザー認証、データ取得までの過程は以下の記事でまとめられていたので、こちらをご覧ください。

https://zenn.dev/kou_pg_0131/articles/tanita-health-planet-api

#### 直近2週間分の体重データを取得

次にデータを取得します。
言語はTypeScriptを使いました。（言語はなんでもいいと思います）

今回、実行環境はブラウザではなくNode.jsを想定しているため、ブラウザのみでサポートされているJavaScript標準の`fetch`メソッドではなく、[axios](https://axios-http.com/) で、データフェッチを行いました。

また、日時の処理は「[date-fns](https://date-fns.org/)」を使用しています。

```ts
/** 体重データを取得 */
export const fetchWeight = async () => {
  /** Health Planet API URL */
  const url = "https://www.healthplanet.jp/status/innerscan.json";

  /** 現在のJST時間を取得 */
  const jstNow = utcToZonedTime(new Date(), "Asia/Tokyo");

  /** 14日前（YYYYMMDD000000） */
  const date14DaysAgo = format(subDays(jstNow, 14), "yyyyMMdd") + "000000";
  /** 1日前（YYYYMMDD235959） */
  const date1DayAgo = format(subDays(jstNow, 1), "yyyyMMdd") + "235959";

  const params = {
    access_token: process.env.HEALTH_PLANET_ACCESS_TOKEN,
    date: "1",
    from: date14DaysAgo,
    to: date1DayAgo,
    tag: "6021",
  };

  try {
    const response = await axios.get(url, { params });
    if (response.status !== 200) {
      throw new Error("Network response was not ok");
    }
    return response.data;
  } catch (error) {
    console.error("Error fetching data from Health Planet API", error);
    throw error;
  }
};
```
すると、以下のようなデータが返ってきます。（平気で晒す）

```js
{
  birth_date: '19991027',
  data: [
    {
      date: '202312090750', // 2023年12月9日7時50分
      keydata: '74.10', // 74.10kg
      model: '01000099', // 測定機器名（01000099は、RD-903）
      tag: '6021' // 体重
    },
    {
      date: '202312080728',
      keydata: '73.90',
      model: '01000099',
      tag: '6021'
    },
    // 略
    {
      date: '202311270816', // 2週間前まで取得
      keydata: '74.00',
      model: '01000099',
      tag: '6021'
    }
  ],
  height: '173',
  sex: 'male'
}
```
こちらのデータをもとに、平均値や増減の算出を行っていきます。

### 2. 取得した測定データから平均値と増減の算出

date-fnsの[`isWithinInterval`](https://date-fns.org/v2.16.1/docs/isWithinInterval)メソッドを使い、取得した2週間分のデータから、今週分の体重（1~7日前の体重）と先週分の体重（8~14日前の体重）で分けました。

そこから、今週分の体重の平均と先週分との体重の増減を算出しました。
`calcWeightAverageDiff`関数では、以下のような返り値が返ってきます。

```ts
/** 今週分(1~7日前)の体重の平均と先週分との体重の増減 */
const { currentWeekAverageWeight, weeklyWeightDiff } = calcWeightAverageDiff(fetchData);
console.log(currentWeekAverageWeight, weeklyWeightDiff);

>> 73.63000000000001 -0.15
```
これは直近1週間の平均体重が`73.63kg`、先週の体重平均と比較して`-0.15kg`だったことを表しています。

ここで算出した値を、LINE通知させていきます。

### 3. LINE Notifyでの通知

「LINE Notify」は、文字通りLINEでメッセージを通知するためのLINE公式サービスです。

https://notify-bot.line.me/ja/

#### LINE Notify トークンの発行

LINE Notifyでも、アクセストークンを発行する必要があります。

トークンの発行方法は以下の記事にまとめられていましたので、こちらをご覧ください。

https://zenn.dev/protoout/articles/18-line-notify-setup

#### LINE通知する関数を作成

まず、`今週の体重 XX.Xkg (±〇〇.〇kg)`の形になるように通知するメッセージを整形します。
```ts
/** 通知メッセージの整形 */
export const formatMessage = (currentWeekAverageWeight: number, weeklyWeightDiff: string) => {
  /** 通知メッセージ */
  const message = `\n今週の体重 ${currentWeekAverageWeight.toFixed(2)}kg(${weeklyWeightDiff}kg)`;

  return message;
};
```

整形した通知メッセージを引数に渡して、以下のように`sendLineNotification`関数でLINE通知を行います。
```ts
export const sendLineNotification = async (message: string) => {
  const url = "https://notify-api.line.me/api/notify";
  const token = process.env.LINE_NOTIFY_TOKEN;

  const headers = {
    "Content-Type": "application/x-www-form-urlencoded",
    Authorization: `Bearer ${token}`,
  };

  try {
    const response = await axios.post(url, `message=${encodeURIComponent(message)}`, { headers });
    if (response.status !== 200) {
      throw new Error("Network response was not ok");
    }
  } catch (error) {
    console.log("Error sending LINE notification", error);
    throw error;
  }
};
```

次のステップで、体重取得、計算処理、LINE通知の一連の処理を定期実行させていきます。

### 4. GitHub Actionsで毎朝8時に定期実行させる

当初、cronジョブを設定したり、Macの「[Automator](https://support.apple.com/ja-jp/guide/automator/welcome/mac)」を使用して、定期実行させていました。ただこれらの方法だと、JavaScriptを実行するコンピューターがスリープ状態のときは、実行できないという懸念点がありました。

そこで、GitHub Actionsの`schedule`イベントで毎朝8時に定期実行させようと考えました。

#### アクセストークンをSecretsに追加

コードでは、Health Planet API と LINE Notify のトークンを環境変数にしているので、GitHub Actionsでも環境変数を使用するために、リポジトリのセキュリティ設定に値を追加する必要があります。

以下の手順で追加
1. タブ「Settings」を選択
1. サイドバー「Secrets and variables」から「Actions」を選択
1. 「New repository secret」で追加

以下のようになればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635876/c010d90c-2314-aa04-dfa5-cb4a489a1fab.png)

#### ワークフローの作成

協定世界時（UTC）で23時（日本標準時で翌朝の8時）に定期実行させるようにワークフローを作成して、Pushします。

```yml
name: Daily Weight Notification

on:
  schedule:
    - cron: "0 23 * * *" # UTCで毎日23時（JSTで翌日の8時）
  workflow_dispatch:

jobs:
  build-and-run:
    runs-on: ubuntu-latest

    env: # 環境変数を設定
      LINE_NOTIFY_TOKEN: ${{ secrets.LINE_NOTIFY_TOKEN }}
      HEALTH_PLANET_ACCESS_TOKEN: ${{ secrets.HEALTH_PLANET_ACCESS_TOKEN }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm install

      - name: Compile TypeScript
        run: npm run build # tscを実行するように、事前にbuildコマンドを用意

      - name: Run script
        run: node dist/index.js
```

そうすると、リポジトリのActionsタブから定期実行されていることが確認できました。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635876/952b31d6-9f5b-a591-31e7-275c7e1a5eb8.png)



## 感想

感想としては、「便利すぎる」という点に尽きます。

実際に今年の夏、人生で初めて本格的な減量を行いましたが、12~3キロほど、計画的に減量させることができました。もちろん減量期だけではなく、増量期にも使っています。

今回の開発で、日常の「こういうことできたらいいけど、現状ちょっと足りないな...」というかゆい機能を自分でプラスできるのは、開発してて楽しいなと感じました。

あと、ドヤ顔で晒せるぐらいムキムキになりたい.....。


## 補足

使用している体重計

**innerscanDUAL RD-903**

https://www.tanita.co.jp/product/bodycompositionmonitor/1883/