Xiaomi Smart Band 7をGadgetbridgeに移行してInfluxDB/Grafanaで可視化

スポンサーリンク

これは、Xiaomi Smart Band 7 のデータを Gadgetbridge 経由で取り出し、InfluxDB に保存して Grafana で可視化するまでの個人的な作業メモです。

作業自体は、デバッグモードにしたスマホをUSBでつなぎ、Codex GPT-5.5 xhighに自由にadbコマンドをたたいてもらって95%くらい自律的にやってもらいました。

もし役立てばということで記録します。

スポンサーリンク

やりたかったこと

Xiaomi Smart Band 7 は小さくて軽く、電池持ちもよく、日常的に着けっぱなしにしやすいスマートバンドです。

ただ、歩数、心拍、睡眠、SpO2 などのデータは基本的に Zepp Life や Mi Fitness のアプリ内で見る前提になっています。自分の用途では、これを外へ出して、InfluxDB に蓄積し、Grafana で長期的に見たいという目的がありました。

今回の方針はこうです。

  1. Xiaomi Smart Band 7 を Zepp Life から Gadgetbridge 運用へ移す
  2. Gadgetbridge でアクティビティデータを同期する
  3. Gadgetbridge の SQLite DB を Android スマホ内に自動エクスポートする
  4. Syncthing-Fork でその DB をサーバーへ同期する
  5. サーバー側で SQLite を読んで InfluxDB に投入する
  6. Grafana でダッシュボード化する

全体像はこうなりました。

flowchart LR
    band["Xiaomi Smart Band 7"] -->|Bluetooth| phone["Galaxy S24 Ultra<br/>Gadgetbridge"]
    phone -->|Auto fetch activity data| gbdb["Gadgetbridge internal DB"]
    gbdb -->|Auto export database| exportdb["/sdcard/Documents/Gadgetbridge/Gadgetbridge.db"]
    exportdb -->|Syncthing-Fork| server["Orion<br/>Syncthing receive-only folder"]
    server --> importer["gb2influx<br/>Docker container"]
    importer --> influx["InfluxDB"]
    influx --> grafana["Grafana dashboard"]

スマホは Bluetooth 同期と DB エクスポートだけを担当し、InfluxDB token を持たせない構成にしました。Python で DB を読んで InfluxDB に書き込む処理は、Android ではなく Linux サーバー側で動かします。

環境

今回確認した環境です。

Phone: Galaxy S24 Ultra
Android: 16
Model: SM-S928Q
Band: Xiaomi Smart Band 7
Gadgetbridge: nodomain.freeyourgadget.gadgetbridge 0.91.1
Zepp Life: com.xiaomi.hm.health 6.16.0
ADB: Windows platform-tools from WSL

ADB は WSL から Windows 側の platform-tools を呼び出しました。

/mnt/c/Software/platform-tools/adb.exe

重要な注意点

Smart Band 7 を Gadgetbridge で使うには、Huami/Xiaomi の auth key が必要です。

この auth key を取得する前に Zepp Life アプリ側でバンドを解除すると、キーが無効になり、やり直しになります。Android の Bluetooth 設定からペアリング情報を消すことと、Zepp Life アプリ内でデバイス解除することは別物です。

今回やったこと、やらなかったことはこうです。

OK:
  - Zepp Life を強制停止する
  - Zepp Life を disable-user で無効化する
  - Gadgetbridge 接続成功後に Zepp Life を止める
  - Android OS 側の Bluetooth ペアリング情報を必要に応じて整理する

NG:
  - auth key 取得前に Zepp Life アプリ内でバンドを解除する
  - バンド本体を初期化する

Auth key 自体はこの記事には載せません。

Zepp Life を止める

auth key を取得し、Gadgetbridge 側へ移行する準備ができたので、Zepp Life を強制停止して無効化しました。

/mnt/c/Software/platform-tools/adb.exe shell am force-stop com.xiaomi.hm.health
/mnt/c/Software/platform-tools/adb.exe shell pm disable-user --user 0 com.xiaomi.hm.health

無効化されていることを確認します。

/mnt/c/Software/platform-tools/adb.exe shell cmd package list packages -d

実行結果です。

package:com.xiaomi.hm.health

戻す場合は以下です。

/mnt/c/Software/platform-tools/adb.exe shell pm enable com.xiaomi.hm.health

Gadgetbridge にペアリングする

Gadgetbridge 側では、次の流れで Smart Band 7 を追加しました。

  1. Gadgetbridge を開く
  2. Device discovery を開く
  3. Start discovery を押す
  4. Smart Band 7 が見つかる
  5. デバイス行を長押しする
  6. Authentication settings を開く
  7. auth key を入力する
  8. Submit する
  9. デバイス行を選択する
  10. Android の Bluetooth ペアリング確認を許可する
  11. Companion device permission を許可する
  12. バンド側に確認が出たら承認する

接続後、Gadgetbridge のメイン画面に Smart Band 7 が表示され、歩数、距離、睡眠などの値が出るようになりました。

この時点で、Zepp Life は「アプリ内で解除した」のではなく、Android 側で止めただけです。

Gadgetbridge の自動同期設定

Gadgetbridge では、まずアクティビティデータの自動取得を有効化しました。

Settings -> Automations -> Auto fetch activity data

これは、バンドからスマホ上の Gadgetbridge へデータを取り込む設定です。サーバーへの送信ではありません。

次に DB の自動エクスポートを有効化しました。

Settings -> Automations -> Auto export database

エクスポート先は以下にしました。

/sdcard/Documents/Gadgetbridge/Gadgetbridge.db

Android のパスとしては、こちらと同じ場所です。

/storage/emulated/0/Documents/Gadgetbridge/Gadgetbridge.db

ここで重要なのは、Auto export database はあくまで「スマホ内の通常ストレージへ SQLite DB を書き出す」だけということです。サーバーへ自動で送るには、Syncthing などの別アプリが必要です。

エクスポート間隔は最終的に 3 時間にしました。健康データの可視化であればリアルタイム性はそこまで必要ないので、まずはこのくらいで十分と判断しました。反映遅延は、ざっくり「Gadgetbridge のエクスポート間隔 + Syncthing 同期 + importer の実行間隔」です。

スマホ内の DB を確認する

ADB で、スマホ内に DB が出ているか確認しました。

/mnt/c/Software/platform-tools/adb.exe shell '
for d in /sdcard/Documents/Gadgetbridge /storage/emulated/0/Documents/Gadgetbridge; do
  echo DIR:$d
  ls -la "$d" 2>&1
done
' | tr -d '\r'

実行結果です。

DIR:/sdcard/Documents/Gadgetbridge
total 2740
-rw-rw---- 1 u0_a293 media_rw 2805760 2026-06-13 18:40 Gadgetbridge.db

DIR:/storage/emulated/0/Documents/Gadgetbridge
total 2740
-rw-rw---- 1 u0_a293 media_rw 2805760 2026-06-13 18:40 Gadgetbridge.db

DB ファイルを探す場合は、こうしました。

/mnt/c/Software/platform-tools/adb.exe shell '
find /sdcard/Documents -maxdepth 4 \( -iname "*gadget*" -o -iname "*.zip" -o -iname "*.db" \) -print 2>/dev/null
' | tr -d '\r'

実行結果です。

/sdcard/Documents/Gadgetbridge
/sdcard/Documents/Gadgetbridge/Gadgetbridge.db

SQLite DB を WSL にコピーして中身を見る

初回確認では、Syncthing ではなく ADB で直接 DB を取り出しました。

adb pull で WSL 側のパスにうまく書けないことがあったので、今回は exec-out cat を使いました。

rm -rf /tmp/gadgetbridge-check
mkdir -p /tmp/gadgetbridge-check

/mnt/c/Software/platform-tools/adb.exe exec-out \
  cat /sdcard/Documents/Gadgetbridge/Gadgetbridge.db \
  > /tmp/gadgetbridge-check/Gadgetbridge.db

ls -lh /tmp/gadgetbridge-check/Gadgetbridge.db
sqlite3 /tmp/gadgetbridge-check/Gadgetbridge.db 'PRAGMA integrity_check;'
sqlite3 /tmp/gadgetbridge-check/Gadgetbridge.db '.tables'

実行結果です。

-rw-r--r-- 1 ctrluser ctrluser 2.7M Jun 13 18:41 /tmp/gadgetbridge-check/Gadgetbridge.db
ok

実際に入っていたテーブル

Smart Band 7 のデータは、今回の DB では XIAOMI_* 系ではなく、主に HUAMI_* 系のテーブルに入っていました。

件数はこうでした。

65172 HUAMI_EXTENDED_ACTIVITY_SAMPLE
 1599 HUAMI_SPO2_SAMPLE
  993 HUAMI_STRESS_SAMPLE
   40 HUAMI_PAI_SAMPLE
   26 BATTERY_LEVEL
    6 HUAMI_HEART_RATE_RESTING_SAMPLE

一方で、以下のテーブルは空でした。

XIAOMI_ACTIVITY_SAMPLE
XIAOMI_SLEEP_STAGE_SAMPLE
XIAOMI_SLEEP_TIME_SAMPLE
XIAOMI_DAILY_SUMMARY_SAMPLE
XIAOMI_MANUAL_SAMPLE
MI_BAND_ACTIVITY_SAMPLE

ここは Gadgetbridge のバージョン、デバイス、移行経路によって変わる可能性があるので、最初に .tablesPRAGMA table_info(...) を見るのが安全です。

歩数・心拍のテーブル

メインの活動データは HUAMI_EXTENDED_ACTIVITY_SAMPLE に入っていました。

sqlite3 /tmp/gadgetbridge-check/Gadgetbridge.db \
  'PRAGMA table_info(HUAMI_EXTENDED_ACTIVITY_SAMPLE);'

カラムは以下です。

TIMESTAMP
DEVICE_ID
USER_ID
RAW_INTENSITY
STEPS
RAW_KIND
HEART_RATE
UNKNOWN1
SLEEP
DEEP_SLEEP
REM_SLEEP

データ範囲と件数を確認しました。

sqlite3 -header -column /tmp/gadgetbridge-check/Gadgetbridge.db "
SELECT
  datetime(MIN(TIMESTAMP),'unixepoch','localtime') AS first_time,
  datetime(MAX(TIMESTAMP),'unixepoch','localtime') AS last_time,
  COUNT(*) AS rows,
  SUM(STEPS) AS total_steps,
  SUM(CASE WHEN HEART_RATE > 0 AND HEART_RATE != 255 THEN 1 ELSE 0 END) AS valid_hr_rows
FROM HUAMI_EXTENDED_ACTIVITY_SAMPLE;
"

実行結果です。

first_time           last_time            rows   total_steps  valid_hr_rows
2026-04-29 11:58:00  2026-06-13 18:10:00  65172  98252        14867

日別に見るとこうです。

sqlite3 -header -column /tmp/gadgetbridge-check/Gadgetbridge.db "
SELECT
  date(TIMESTAMP,'unixepoch','localtime') AS day,
  SUM(STEPS) AS steps,
  COUNT(*) AS rows,
  SUM(CASE WHEN HEART_RATE > 0 AND HEART_RATE != 255 THEN 1 ELSE 0 END) AS valid_hr_rows,
  MIN(NULLIF(HEART_RATE,255)) AS min_hr,
  MAX(CASE WHEN HEART_RATE != 255 THEN HEART_RATE END) AS max_hr
FROM HUAMI_EXTENDED_ACTIVITY_SAMPLE
GROUP BY day
ORDER BY day DESC
LIMIT 10;
"

実行結果の一部です。

day         steps  rows  valid_hr_rows  min_hr  max_hr
2026-06-13  2283   1091  609            55      141
2026-06-12  9      1440  348            56      111
2026-06-11  9      1440  324            56      113
2026-06-10  30     1440  341            54      102
2026-06-09  25     1440  388            53      106
2026-06-08  3459   1440  607            51      134

直近の有効な心拍行も見ました。

sqlite3 -header -column /tmp/gadgetbridge-check/Gadgetbridge.db "
SELECT
  datetime(TIMESTAMP,'unixepoch','localtime') AS local_time,
  STEPS,
  HEART_RATE,
  RAW_KIND,
  RAW_INTENSITY
FROM HUAMI_EXTENDED_ACTIVITY_SAMPLE
WHERE HEART_RATE > 0 AND HEART_RATE != 255
ORDER BY TIMESTAMP DESC
LIMIT 20;
"

実行結果の一部です。

local_time           STEPS  HEART_RATE  RAW_KIND  RAW_INTENSITY
2026-06-13 17:44:00  0      141         67        52
2026-06-13 17:43:00  31     140         64        63
2026-06-13 17:42:00  102    125         64        94

HEART_RATE = 255 は心拍なしとして扱い、InfluxDB へは入れないようにしました。

SpO2・ストレス・安静時心拍・バッテリー

SpO2 は HUAMI_SPO2_SAMPLE に入っていました。

sqlite3 /tmp/gadgetbridge-check/Gadgetbridge.db \
  'PRAGMA table_info(HUAMI_SPO2_SAMPLE);'
TIMESTAMP
DEVICE_ID
USER_ID
TYPE_NUM
SPO2

ここで注意が必要なのは、TIMESTAMP が13桁のミリ秒タイムスタンプだったことです。Unix 秒にするには /1000 します。

sqlite3 -header -column /tmp/gadgetbridge-check/Gadgetbridge.db "
SELECT
  datetime(TIMESTAMP/1000,'unixepoch','localtime') AS local_time,
  TYPE_NUM,
  SPO2
FROM HUAMI_SPO2_SAMPLE
ORDER BY TIMESTAMP DESC
LIMIT 10;
"

実行結果の一部です。

local_time           TYPE_NUM  SPO2
2026-06-13 12:55:30  0         97
2026-06-13 12:50:30  0         98
2026-06-13 12:45:30  0         97
2026-06-13 12:40:30  0         94

ストレスは HUAMI_STRESS_SAMPLE です。これもミリ秒タイムスタンプでした。

sqlite3 -header -column /tmp/gadgetbridge-check/Gadgetbridge.db "
SELECT
  datetime(TIMESTAMP/1000,'unixepoch','localtime') AS local_time,
  TYPE_NUM,
  STRESS
FROM HUAMI_STRESS_SAMPLE
ORDER BY TIMESTAMP DESC
LIMIT 10;
"
local_time           TYPE_NUM  STRESS
2026-06-13 13:06:00  1         10
2026-06-13 12:56:00  1         29
2026-06-13 12:51:00  1         20

安静時心拍は HUAMI_HEART_RATE_RESTING_SAMPLE です。

sqlite3 -header -column /tmp/gadgetbridge-check/Gadgetbridge.db "
SELECT
  datetime(TIMESTAMP/1000,'unixepoch','localtime') AS local_time,
  UTC_OFFSET,
  HEART_RATE
FROM HUAMI_HEART_RATE_RESTING_SAMPLE
ORDER BY TIMESTAMP DESC
LIMIT 10;
"
local_time           UTC_OFFSET  HEART_RATE
2026-06-13 12:56:00  32400000    61
2026-06-12 23:59:00  32400000    61
2026-06-11 23:59:00  32400000    60

バッテリーは BATTERY_LEVEL に入っていました。こちらは Unix 秒でした。

sqlite3 -header -column /tmp/gadgetbridge-check/Gadgetbridge.db "
SELECT
  datetime(TIMESTAMP,'unixepoch','localtime') AS local_time,
  LEVEL,
  BATTERY_INDEX
FROM BATTERY_LEVEL
ORDER BY TIMESTAMP DESC
LIMIT 10;
"
local_time           LEVEL  BATTERY_INDEX
2026-06-13 18:38:42  84     0
2026-06-13 18:37:42  83     0
2026-06-13 18:36:42  81     0

Syncthing-Fork でスマホからサーバーへ送る

Gadgetbridge の DB エクスポートはスマホ内で完結します。そこで、Android 側に Syncthing-Fork を入れ、サーバー側の Syncthing と同期させました。

Android 側は以下です。

package: com.github.catfriend1.syncthingfork
version: 2.1.1.0

このときは F-Droid の APK を ADB で入れました。

wget -O /tmp/syncthingfork.apk \
  https://f-droid.org/repo/com.github.catfriend1.syncthingfork_2010100.apk

/mnt/c/Software/platform-tools/adb.exe install "$(wslpath -w /tmp/syncthingfork.apk)"

通知権限やバックグラウンド動作まわりも ADB から設定しました。

/mnt/c/Software/platform-tools/adb.exe shell pm grant \
  com.github.catfriend1.syncthingfork android.permission.POST_NOTIFICATIONS
/mnt/c/Software/platform-tools/adb.exe shell pm grant \
  com.github.catfriend1.syncthingfork android.permission.ACCESS_FINE_LOCATION
/mnt/c/Software/platform-tools/adb.exe shell appops set \
  com.github.catfriend1.syncthingfork MANAGE_EXTERNAL_STORAGE allow
/mnt/c/Software/platform-tools/adb.exe shell dumpsys deviceidle whitelist \
  +com.github.catfriend1.syncthingfork

Android 側の Syncthing-Fork では、次のフォルダを共有しました。

label: Gadgetbridge
id: gadgetbridge
directory: /storage/emulated/0/Documents/Gadgetbridge

サーバー側は、Syncthing の gadgetbridge フォルダを receive-only にしました。

id: gadgetbridge
label: Gadgetbridge
path inside container: /var/syncthing/Gadgetbridge
host path: /home/ctrluser/docker/gb2influx/data
type: receiveonly

receive-only にした理由は、Android/Gadgetbridge 側を正とし、サーバー側の古い DB をスマホへ押し戻さないためです。

同期後、サーバー側には以下のように DB が届きました。

ssh orion '
ls -la ~/docker/gb2influx/data
find ~/docker/gb2influx/data -maxdepth 1 -type f \
  -printf "%f %s bytes %TY-%Tm-%Td %TH:%TM:%TS\n"
'

実行結果です。

Gadgetbridge.db 2805760 bytes 2026-06-13 09:40:15...

Linux 側の find では UTC 表示なので、これは Android 側の 2026-06-13 18:40 JST のエクスポートに対応します。

Syncthing のフォルダ状態も正常でした。

state: idle
globalFiles: 1
globalBytes: 2805760
localFiles: 1
localBytes: 2805760
needFiles: 0
needBytes: 0

InfluxDB に入れるデータ構造

InfluxDB 側は、最初から細かくやりすぎず、測定値ごとに measurement を分けました。

wearable_activity
  tags:
    source=gadgetbridge
    device=xiaomi_smart_band_7
  fields:
    steps
    heart_rate_bpm
    raw_intensity
    raw_kind
    sleep
    deep_sleep
    rem_sleep

wearable_spo2
  tags:
    source=gadgetbridge
    device=xiaomi_smart_band_7
    type_num=<TYPE_NUM>
  fields:
    spo2_percent

wearable_stress
  tags:
    source=gadgetbridge
    device=xiaomi_smart_band_7
    type_num=<TYPE_NUM>
  fields:
    stress

wearable_resting_heart_rate
  tags:
    source=gadgetbridge
    device=xiaomi_smart_band_7
  fields:
    heart_rate_bpm

wearable_battery
  tags:
    source=gadgetbridge
    device=xiaomi_smart_band_7
    battery_index=<BATTERY_INDEX>
  fields:
    level_percent

タイムスタンプの扱いは以下です。

HUAMI_EXTENDED_ACTIVITY_SAMPLE.TIMESTAMP: Unix seconds
BATTERY_LEVEL.TIMESTAMP: Unix seconds
HUAMI_SPO2_SAMPLE.TIMESTAMP: Unix milliseconds
HUAMI_STRESS_SAMPLE.TIMESTAMP: Unix milliseconds
HUAMI_HEART_RATE_RESTING_SAMPLE.TIMESTAMP: Unix milliseconds

Importer では、毎回直近 72 時間を再処理するようにしました。Gadgetbridge 側の同期遅延や睡眠データの後補正を拾うためです。

importer の配置

importer はスマホではなく、サーバー側で Docker コンテナとして動かしました。

配置はこうです。

Galaxy S24 Ultra
  /sdcard/Documents/Gadgetbridge/Gadgetbridge.db
  -> Syncthing-Fork

Orion
  /home/ctrluser/docker/gb2influx/data/Gadgetbridge.db
  /home/ctrluser/docker/gb2influx/app/gb2influx.py
  /home/ctrluser/docker/gb2influx/state/state.json
  -> writes to InfluxDB

InfluxDB / Grafana
  wearable_* measurements

手動実行は以下です。

ssh orion 'cd ~/docker/gb2influx && docker compose run --rm -e RUN_ONCE=1 gb2influx'

常駐実行は以下です。

ssh orion 'cd ~/docker/gb2influx && docker compose up -d'

初回の historical import では、以下の結果になりました。

written_points=67796 db=/data/Gadgetbridge.db

Syncthing で DB が届いたあとの再実行では、以下です。

written_points=4685 db=/data/Gadgetbridge.db

2回目の件数が少ないのは、state file を持っていて、初回の全量投入後は直近ウィンドウだけを再処理しているためです。

InfluxDB 側では、以下の measurement が確認できました。

wearable_activity
wearable_battery
wearable_resting_heart_rate
wearable_spo2
wearable_stress

件数確認では、以下のようになりました。

wearable_activity             405899 field values
wearable_battery                  26 field values
wearable_resting_heart_rate        6 field values
wearable_spo2                   1599 field values
wearable_stress                  993 field values

wearable_activity が SQLite の行数より多く見えるのは、InfluxDB 側では field value 単位で数えているためです。1行の SQLite レコードから複数 field が入るので、元の行数とは一致しません。

Grafana ダッシュボード

Grafana では、最初のダッシュボードとして以下を作りました。

uid: wearable-health-smart-band-7
title: Wearable Health - Smart Band 7
folder: Studio-managed
panels: 11

最初に見るパネルは、次のあたりで十分だと思います。

  1. 日次歩数
  2. 心拍の時系列
  3. SpO2 の時系列
  4. ストレスの時系列
  5. バッテリー残量
  6. 各 measurement の最新 timestamp

睡眠については、HUAMI_EXTENDED_ACTIVITY_SAMPLESLEEP, DEEP_SLEEP, REM_SLEEP のカラムがあります。ただ、これらの値の意味は Gadgetbridge の UI 表示と照合してから使った方がよさそうです。少なくとも最初のダッシュボードでは、睡眠ステージをきれいに解釈するより、就寝中の心拍、心拍の低下、起床前の上昇、データ欠損を見る方が安全だと思います。

現時点の状態

2026-06-13 時点で、状態はこうです。

Gadgetbridge pairing: OK
Zepp Life: disabled, not unpaired
Auto fetch activity data: enabled
Auto export database: enabled
Exported DB: /sdcard/Documents/Gadgetbridge/Gadgetbridge.db
DB integrity_check: ok
Syncthing-Fork: Android -> Orion sync OK
Orion Syncthing folder: idle / in-sync
Importer: Docker container running
InfluxDB measurements: wearable_activity, wearable_battery, wearable_resting_heart_rate, wearable_spo2, wearable_stress
Grafana dashboard: deployed

残っている作業は、数日から1週間ほど運用して、同期抜け、エクスポート間隔、バッテリー消費、睡眠データの解釈を確認することです。

感想

今回の構成で分かったのは、Xiaomi Smart Band 7 は Gadgetbridge との相性がかなりよく、買い替え前に試す価値が高いということです。

一方で、Gadgetbridge へつながっただけで自動的に InfluxDB まで流れるわけではありません。実際のワークフローは、バンド、Android、Gadgetbridge、DB export、Syncthing、importer、InfluxDB、Grafana の複数段に分かれます。

ただ、一度流れを作ってしまえば、各段階はかなり素直です。

特に重要なのは、最初に以下を切り分けて考えることでした。

Band -> Gadgetbridge:
  Bluetooth pairing, auth key, activity sync

Gadgetbridge -> Android storage:
  Auto export database

Android storage -> Server:
  Syncthing-Fork

Server -> InfluxDB:
  SQLite importer

InfluxDB -> Grafana:
  Dashboard

ここを混ぜて考えると、「Gadgetbridge に入れたのに Grafana に出ない」のように見えますが、実際には途中にいくつも確認ポイントがあります。

Smart Band 7 をすでに持っていて、Zepp Life のアプリ内だけでデータが閉じるのが不満な場合は、まずこの Gadgetbridge ルートを試すのがよさそうです。

コメント

タイトルとURLをコピーしました