GoでWithings APIのクライアントライブラリを作って体重を取得してLINE通知した

2021/01/28に公開

きっかけ

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をパースする処理で、ファイルの有無などもチェックしていないので呼び出し元で本来はファイル有無を精査する必要がある)

.test_settings.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