GoでWithings APIのクライアントライブラリを作って体重を取得してLINE通知した
きっかけ
Withingsには APIが用意されていて 、スマートウォッチや体重計で取得した情報を取得できる。ダイエットのためにも家族の入っているLINEのトークルームに体重を通知して応援してもらおうと思った。
しかし個人的に好きなGoのクライアントライブラリがNokia時代のものしか見当たらなかったのでライブラリも作成した。
作ったライブラリは以下。(今回はライブラリの説明は必要最低限のみ。使い方はほとんどREADMEに記載しています)
Withings APIを使う準備
Withings developer documentation
Withings APIは自分のアプリを登録し、ユーザにアプリからデータへのアクセス許可をもらう形で利用できる。今回は個人的な用途なので制限(1アプリで5000ユーザまで)は気にしなくても大丈夫。
Withings APIを使うにはアプリの登録よりも先にWithingsのアカウントが必要になるが、Withingsの製品を使っている場合にはすでに持っているはず。
もし持っていない場合は アカウントの作成 から作成できる。
自分のWithingsアカウントでログインした状態で Withings Developer application here にアクセスすれば登録できるはず。
登録完了すればクライアントID、コンシューマーシークレットが取得できる。
LINE Notifyの準備
LINE Notifyについては詳しく解説してくれているページがたくさんあるので省略。
例えば以下。
[超簡単]LINE notify を使ってみる - Qiita
体重取得とLINE通知
利用ライブラリ
LINEの通知には以下のライブラリを利用させてもらった。
utahta/go-linenotify: Go client library for LINE Notify
こちらは自作のWithings APIアクセス用のクライアントライブラリ
コード
package main
import (
"fmt"
"os"
"time"
"github.com/utahta/go-linenotify"
"github.com/zono-dev/withings-go/withings"
)
const (
tokenFile = "access_token.json"
layout = "2006-01-02 15:04:05"
)
var (
jst *time.Location
t time.Time
adayago time.Time
lastupdate time.Time
client *(withings.Client)
settings map[string]string
)
func mainSetup() {
jst = time.FixedZone("Asis/Tokyo", 9*60*60)
// 稼働させているコンテナがUTCなので日本時間に変更
t = (time.Now()).Add(9 * time.Hour)
// to get sample data from 1 days ago to now
adayago = t.Add(-24 * time.Hour)
// lastupdate = withings.OffsetBase
lastupdate = time.Date(2021, 01, 26, 0, 0, 0, 0, time.UTC)
}
func auth() {
var err error
client, err = withings.New(settings["CID"], settings["Secret"], settings["RedirectURL"])
if err != nil {
fmt.Println("Failed to create New client")
fmt.Println(err)
return
}
if _, err := os.Open(tokenFile); err != nil {
var e error
client.Token, e = withings.AuthorizeOffline(client.Conf)
if e != nil {
fmt.Println("Failed to authorize offline.")
}
client.Client = withings.GetClient(client.Conf, client.Token)
} else {
_, err = client.ReadToken(tokenFile)
if err != nil {
fmt.Println("Failed to read token file.")
fmt.Println(err)
return
}
}
}
func tokenFuncs() {
// Show token
client.PrintToken()
// Refresh Token if you need
_, rf, err := client.RefreshToken()
if err != nil {
fmt.Println("Failed to RefreshToken")
fmt.Println(err)
return
}
if rf {
fmt.Println("You got new token!")
client.PrintToken()
}
// Save Token if you need
err = client.SaveToken(tokenFile)
if err != nil {
fmt.Println("Failed to RefreshToken")
fmt.Println(err)
return
}
}
func notifyLine(message string) {
c := linenotify.New()
resp, _ := c.Notify(settings["LineToken"], message, "", "", nil)
fmt.Println(resp)
}
func createMessage(v withings.MeasureData, name, unit string) string {
return fmt.Sprintf("%v %s %.1f %s\n", v.Date.In(jst).Format(layout), name, v.Value, unit)
}
func getmeas() {
fmt.Println("========== Getmeas[START] ========== ")
mym, err := client.GetMeas(withings.Real, adayago, t, lastupdate, 0, false, true, withings.Weight)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Status: %d\n", mym.Status)
for _, v := range mym.SerializedData.Weights {
m := createMessage(v, "Weight", "Kg")
notifyLine(m)
}
fmt.Println("========== Getmeas[END] ========== ")
}
func main() {
settings = withings.ReadSettings(".test_settings.yaml")
auth()
tokenFuncs()
mainSetup()
getmeas()
}
認証
auth
内での以下のブロックで初回、もしくは2回目以降(すでにトークンファイルがある場合)の処理を行っている。
if _, err := os.Open(tokenFile); err != nil {
var e error
client.Token, e = withings.AuthorizeOffline(client.Conf)
if e != nil {
fmt.Println("Failed to authorize offline.")
}
client.Client = withings.GetClient(client.Conf, client.Token)
} else {
_, err = client.ReadToken(tokenFile)
if err != nil {
fmt.Println("Failed to read token file.")
fmt.Println(err)
return
}
}
tokenFuncs
内でトークンのリフレッシュおよび指定されたファイル名でトークンファイルの保存をしている。
トークンをリフレッシュする必要があるかどうかは実際には withings.RefreshToken
を呼んだ結果で分かるので、API呼び出し前に毎度呼ぶほうが良い気がする。
// Refresh Token if you need
_, rf, err := client.RefreshToken()
if err != nil {
fmt.Println("Failed to RefreshToken")
fmt.Println(err)
return
}
if rf {
fmt.Println("You got new token!")
client.PrintToken()
}
// Save Token if you need
err = client.SaveToken(tokenFile)
if err != nil {
fmt.Println("Failed to RefreshToken")
fmt.Println(err)
return
}
クライアントID、コンシューマーシークレット、リダイレクトURL、LINEのトークンは .test_settings.yaml
に定義しておく。
withings.ReadSettings
で yamlを読み込めるようにしている(が、中身はただのyamlをパースする処理で、ファイルの有無などもチェックしていないので呼び出し元で本来はファイル有無を精査する必要がある)
CID: "Your Consumer ID"
Secret: "Your Secret"
RedirectURL: "Your Redirect URL"
LineToken: "Your LINE token"
体重を取得する
getmeas
内で体重を取得している。
withings.GetMeas
には引数が複数あるが、今回は最新のデータだけ取れれば良いので lastupdateに日付を指定することでそれ以降のデータを取得する。
Withings APIの仕様として、startdate, enddateを指定してその日時範囲内のデータを取得するか、lastupdateに日付を指定してそれ以降のデータを取得するかの二通りがある。
ライブラリを作るときに悩んだのだが、lastupdateを優先する仕様としてlastupdateに OffsetBase (中身は time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) )を指定してあれば startdate, enddateを優先するようにした(もっといい方法があれば教えてください)
GetMeasの引数の詳細は withings GetMeas · pkg.go.dev を参照のこと。
最後の引数は可変長引数として取得したいデータを指定できるのだが、Withings APIは一つだけ指定しても複数指定しても全部の値を返してくるようだ。
mym, err := client.GetMeas(withings.Real, adayago, t, lastupdate, 0, false, true, withings.Weight)
if err != nil {
fmt.Println(err)
return
}
for _, v := range mym.SerializedData.Weights {
m := createMessage(v, "Weight", "Kg")
notifyLine(m)
}
LINE通知
利用させて頂いたライブラリは非常に簡単に使うことができてとても楽でした。
(本格的に使うならerror処理はしたほうがいいです)
第3、4引数はそれぞれサムネイルのURLとフル画像のURLを入れることができるようだ。また最後の引数を使えば画像データバイナリを直接送ることができる様子。
func notifyLine(message string) {
c := linenotify.New()
resp, _ := c.Notify(settings["LineToken"], message, "", "", nil)
fmt.Println(resp)
}
完成
(体重は恥ずかしいので黒塗りしてます)
Discussion