This commit is contained in:
shoopea 2023-02-17 18:30:16 +08:00
commit 6bd83d373a
10 changed files with 1726 additions and 0 deletions

360
client.go Normal file
View File

@ -0,0 +1,360 @@
// license that can be found in the LICENSE file.
// Package withings is UNOFFICIAL sdk of withings API for Go client.
//
package withings
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"golang.org/x/oauth2"
"gopkg.in/yaml.v2"
)
const (
authURL = "http://account.withings.com/oauth2_user/authorize2"
tokenURL = "https://wbsapi.withings.net/v2/oauth2"
defaultTokenFile = ".access_token.json"
)
// Client type
type Client struct {
Client *http.Client
Conf *oauth2.Config
Token *oauth2.Token
Timeout time.Duration
MeasureURL string
MeasureURLv2 string
SleepURLv2 string
}
// ClientOption type for to customize http.Client
type ClientOption func(*http.Client) error
// AuthorizeOffline provides oauth2 authorization for withings in CLI.
// See example/main.go to know the detail.
func AuthorizeOffline(conf *oauth2.Config) (*oauth2.Token, error) {
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
fmt.Printf("URL to authorize:%s\n", url)
var grantcode string
fmt.Printf("Open url your browser and Enter your grant code here.\n Grant Code:")
fmt.Scan(&grantcode)
token, err := conf.Exchange(newOauthContext(), grantcode)
if err != nil {
fmt.Println("Failed to oauth2 exchange.")
return nil, err
}
return token, nil
}
// ReadSettings read setting file which is yaml file and returns the settings.
func ReadSettings(path2settings string) map[string]string {
f, err := os.Open(path2settings)
if err != nil {
log.Fatal(err)
}
defer f.Close()
d := yaml.NewDecoder(f)
var m map[string]string
if err := d.Decode(&m); err != nil {
log.Fatal(err)
}
return m
}
// getNewContext returns new context that has Timeout settings.
func getNewContext(timeout time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), timeout)
}
// New returns new client.
// cid is client id, secret and redirectURL are parameters that you got them when you setup withings API.
func New(cid, secret, redirectURL string, options ...ClientOption) (*Client, error) {
conf := GetNewConf(cid, secret, redirectURL)
c := &Client{}
c.Conf = &conf
c.Token = &oauth2.Token{}
c.Client = GetClient(c.Conf, c.Token)
c.Timeout = 5 * time.Second
c.MeasureURL = defaultMeasureURL
c.MeasureURLv2 = defaultMeasureURLv2
c.SleepURLv2 = defaultSleepURLv2
for _, option := range options {
err := option(c.Client)
if err != nil {
return nil, err
}
}
return c, nil
}
// SetScope sets scope for oauth2 client.
func (c *Client) SetScope(scopes ...string) {
c.Conf.Scopes = []string{strings.Join([]string(scopes), ",")}
}
// SetTimeout sets timeout setting for http client.
func (c *Client) SetTimeout(timeout time.Duration) {
c.Timeout = timeout * time.Second
}
// PrintTimeout print timeout setting.
func (c *Client) PrintTimeout() {
fmt.Printf("Timeout=%v\n", c.Timeout)
}
// ReadToken read from a file and that token is set to client.
func (c *Client) ReadToken(path2file string) (*oauth2.Token, error) {
t, err := readToken(path2file)
if err != nil {
return nil, err
}
c.Token = t
c.Client = GetClient(c.Conf, c.Token)
return c.Token, nil
}
func readToken(path2file string) (*oauth2.Token, error) {
token := &oauth2.Token{}
file, err := os.Open(path2file)
if err == nil {
json.NewDecoder(file).Decode(token)
}
return token, err
}
// SaveToken save the token in the file.
func (c *Client) SaveToken(path2file string) error {
var fname string = path2file
if fname == "" {
fname = defaultTokenFile
}
return saveToken(c.Token, fname)
}
func saveToken(t *oauth2.Token, path2file string) error {
file, err := os.Create(path2file)
if err != nil {
return err
}
err = json.NewEncoder(file).Encode(t)
if err != nil {
return err
}
return nil
}
// RefreshToken get new token if necessary.
func (c *Client) RefreshToken() (*oauth2.Token, bool, error) {
newToken, err := refreshToken(c.Conf, c.Token)
if err != nil {
return nil, false, err
}
var isNewToken bool = false
if newToken != c.Token {
c.Token = newToken
c.Client = GetClient(c.Conf, c.Token)
isNewToken = true
}
return c.Token, isNewToken, nil
}
func refreshToken(conf *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
newToken, err := (conf.TokenSource(newOauthContext(), token).Token())
if err != nil {
return nil, err
}
return newToken, nil
}
// Do is just call `Do` of http.Client.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
ret, err := c.Client.Do(req)
return ret, err
}
// GetNewConf returns oauth2.Config with client id, secret, and redirectURL
func GetNewConf(cid, secret, redirectURL string) oauth2.Config {
scopes := []string{ScopeActivity, ScopeMetrics, ScopeInfo}
conf := oauth2.Config{
RedirectURL: redirectURL,
ClientID: cid,
ClientSecret: secret,
Scopes: []string{strings.Join([]string(scopes), ",")},
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
AuthStyle: oauth2.AuthStyleInParams,
},
}
return conf
}
// GetClient returns *http.Client which based on conf, token.
func GetClient(conf *oauth2.Config, token *oauth2.Token) *http.Client {
client := oauth2.NewClient(context.Background(), conf.TokenSource(newOauthContext(), token))
return client
}
// PrintToken print token information.
func (c *Client) PrintToken() {
printToken(c.Token)
}
func printToken(t *oauth2.Token) {
layout := "2006-01-02 15:04:05"
extraKeys := []string{"access_token", "expires_in", "refresh_token", "scope", "token_type", "userid"}
fmt.Printf("--Token Information--\n")
fmt.Printf("AccessToken:%s\n", t.AccessToken)
fmt.Printf("RefreshToken:%s\n", t.RefreshToken)
fmt.Printf("ExpiryDate:%s\n", t.Expiry.Format(layout))
fmt.Printf("TokenType:%s\n", t.TokenType)
// Extra returns interface{}
for _, k := range extraKeys {
switch val := t.Extra(k).(type) {
case string:
fmt.Printf("%s:%s\n", k, val)
case float64:
fmt.Printf("%s:%g\n", k, val)
default:
if val != nil {
fmt.Println(val)
}
}
}
}
// PrintConf print conf information.
func (c *Client) PrintConf() {
printConf(c.Conf)
}
func printConf(conf *oauth2.Config) {
fmt.Printf("RedirectURL: %v\n", conf.RedirectURL)
fmt.Printf("ClientID: %v\n", conf.ClientID)
fmt.Printf("ClientSecret: %v\n", conf.ClientSecret)
fmt.Printf("Scopes: %v\n", conf.Scopes)
fmt.Printf("Endpoint(AuthURL): %v\n", conf.Endpoint.AuthURL)
fmt.Printf("Endpoint(TokenURL): %v\n", conf.Endpoint.TokenURL)
}
// newOauthContext returns context.Context with
// custom http client for withings's access and refresh tokens endpoints.
func newOauthContext() context.Context {
c := &http.Client{Transport: &oauthTransport{}}
return context.WithValue(context.Background(), oauth2.HTTPClient, c)
}
// oauthTransport is making custom request and response for withings api.
type oauthTransport struct{}
// RoundTrip customize request and response for withings api.
func (t *oauthTransport) RoundTrip(r *http.Request) (*http.Response, error) {
if err := interceptRequest(r); err != nil {
return nil, err
}
res, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
return nil, err
}
if err := interceptResponse(res); err != nil {
return nil, err
}
return res, nil
}
// interceptRequest sets action=requesttoken param.
// this param is required for withings api, but not oauth specification.
func interceptRequest(req *http.Request) error {
if err := req.ParseForm(); err != nil {
return fmt.Errorf("cannot parse request form: %v", err)
}
req.PostForm.Set("action", "requesttoken")
encoded := req.PostForm.Encode()
req.Body = ioutil.NopCloser(strings.NewReader(encoded))
req.ContentLength = int64(len(encoded))
return nil
}
// interceptResponse flattens response.
// withings's response body is nested.
// example)
// from:
// {
// "status": 0,
// "body": {
// "userid": "363",
// "access_token": "a075f8c14fb8df40b08ebc8508533dc332a6910a",
// "refresh_token": "f631236f02b991810feb774765b6ae8e6c6839ca",
// "expires_in": 10800,
// "scope": "user.info,user.metrics",
// "csrf_token": "PACnnxwHTaBQOzF7bQqwFUUotIuvtzSM",
// "token_type": "Bearer"
// }
// }
// to:
// {
// "userid": "363",
// "access_token": "a075f8c14fb8df40b08ebc8508533dc332a6910a",
// "refresh_token": "f631236f02b991810feb774765b6ae8e6c6839ca",
// "expires_in": 10800,
// "scope": "user.info,user.metrics",
// "csrf_token": "PACnnxwHTaBQOzF7bQqwFUUotIuvtzSM",
// "token_type": "Bearer"
// }
func interceptResponse(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return fmt.Errorf("cannot read response body: %v", err)
}
var withingsRes struct {
Status int `json:"status"`
Body json.RawMessage `json:"body"`
Error string `json:"error,omitempty"`
}
err = json.NewDecoder(bytes.NewReader(body)).Decode(&withingsRes)
if err != nil || withingsRes.Error != "" {
return &oauth2.RetrieveError{
Response: res,
Body: body,
}
}
res.Body = ioutil.NopCloser(bytes.NewReader(withingsRes.Body))
res.ContentLength = int64(len(withingsRes.Body))
return nil
}

222
enum.go Normal file
View File

@ -0,0 +1,222 @@
package withings
// API endpoint
const (
defaultMeasureURL = "https://wbsapi.withings.net/measure"
defaultMeasureURLv2 = "https://wbsapi.withings.net/v2/measure"
defaultSleepURLv2 = "https://wbsapi.withings.net/v2/sleep"
)
// scopes
const (
ScopeActivity string = "user.activity"
ScopeMetrics string = "user.metrics"
ScopeInfo string = "user.info"
)
// form keys
const (
PPaction string = "action"
PPmeastype string = "meastype"
PPcategory string = "category"
PPstartdate string = "startdate"
PPenddate string = "enddate"
PPstartdateymd string = "startdateymd"
PPenddateymd string = "enddateymd"
PPlastupdate string = "lastupdate"
PPoffset string = "offset"
PPdataFields string = "data_fields"
)
// Service action
const (
MeasureA string = "getmeas"
ActivityA string = "getactivity"
WorkoutsA string = "getworkouts"
SleepA string = "get"
SleepSA string = "getsummary"
)
// MeasType is Measurement Type
type MeasType int
// Measurement Type
const (
Weight MeasType = 1 // Weight (kg)
Height MeasType = 4 // Height (meter)
FatFreeMass MeasType = 5 // Fat Free Mass (kg)
FatRatio MeasType = 6 // Fat Ratio (%)
FatMassWeight MeasType = 8 // Fat Mass Weight (kg)
DiastolicBP MeasType = 9 // Diastolic Blood Pressure (mmHg)
SystolicBP MeasType = 10 // Systolic Blood Pressure (mmHg)
HeartPulse MeasType = 11 // Heart Pulse (bpm)
Temp MeasType = 12 // Temperature (celsius)
SPO2 MeasType = 54 // SPO2 (%)
BodyTemp MeasType = 71 // Body Temperature (celsius)
SkinTemp MeasType = 73 // Skin temperature (celsius)
MuscleMass MeasType = 76 // Muscle Mass (kg)
Hydration MeasType = 77 // Hydration (kg)
BoneMass MeasType = 88 // Bone Mass (kg)
PWaveVel MeasType = 91 // Pulse Wave Velocity (m/s)
VO2 MeasType = 123 // VO2 max is a numerical measurement of your bodys ability to consume oxygen (ml/min/kg).
)
// CatType is category type
type CatType int
// Category type
const (
Real CatType = 1 // Real is for real measures.
Objective CatType = 2 // Objective is for user objectives.
)
// ActivityType is activity type
type ActivityType string
// Activity Type
const (
Steps ActivityType = "steps" // Number of steps
Distance ActivityType = "distance" // Distance travelled (in meters).
Elevation ActivityType = "elevation" // Number of floors cliembed.
Soft ActivityType = "soft" // Duration of soft activities (in seconds).
Moderate ActivityType = "moderate" // Duration of moderate activities (in seconds).
Intense ActivityType = "intense" // Duration of intense activities (in seconds).
Active ActivityType = "active" // Sum of intense and moderate activity durations (in seconds).
Calories ActivityType = "calories" // Active calories burned (in Kcal).
TotalCalories ActivityType = "totalcalories" // Total calories burned (in Kcal).
HrAverage ActivityType = "hr_average" // Average heart rate.
HrMin ActivityType = "hr_min" // Minimal heart rate.
HrMax ActivityType = "hr_max" // Maximal heart rate.
HrZone0 ActivityType = "hr_zone_0" // Duration in seconds when heart rate was in a light zone.
HrZone1 ActivityType = "hr_zone_1" // Duration in seconds when heart rate was in a moderate zone.
HrZone2 ActivityType = "hr_zone_2" // Duration in seconds when heart rate was in an intense zone.
HrZone3 ActivityType = "hr_zone_3" // Duration in seconds when heart rate was in maximal zone.
)
// WorkoutType is workout type
type WorkoutType string
// Workout Type
const (
WTCalories WorkoutType = "calories" // Active calories burned (in Kcal).
WTEffduration WorkoutType = "effduration" // Effective duration.
WTIntensity WorkoutType = "intensity" // Intensity.
WTManualDistance WorkoutType = "manual_distance" // Distance travelled manually entered by user (in meters).
WTManualCalories WorkoutType = "manual_calories" // Active calories burned manually entered by user (in Kcal).
WTHrAverage WorkoutType = "hr_average" // Average heart rate.
WTHrMin WorkoutType = "hr_min" // Minimal heart rate.
WTHrMax WorkoutType = "hr_max" // Maximal heart rate.
WTHrZone0 WorkoutType = "hr_zone_0" // Duration in seconds when heart rate was in a light zone (cf. Glossary).
WTHrZone1 WorkoutType = "hr_zone_1" // Duration in seconds when heart rate was in a moderate zone (cf. Glossary).
WTHrZone2 WorkoutType = "hr_zone_2" // Duration in seconds when heart rate was in an intense zone (cf. Glossary).
WTHrZone3 WorkoutType = "hr_zone_3" // Duration in seconds when heart rate was in maximal zone (cf. Glossary).
WTPauseDuration WorkoutType = "pause_duration" // Total pause time in second filled by user
WTAlgoPauseDuration WorkoutType = "algo_pause_duration" // Total pause time in seconds detected by Withings device (swim only)
WTSpo2Average WorkoutType = "spo2_average" // Average percent of SpO2 percent value during a workout
WTSteps WorkoutType = "steps" // Number of steps.
WTDistance WorkoutType = "distance" // Distance travelled (in meters).
WTElevation WorkoutType = "elevation" // Number of floors climbed.
WTPoolLaps WorkoutType = "pool_laps" // Number of pool laps.
WTStrokes WorkoutType = "strokes" // Number of strokes.
WTPoolLength WorkoutType = "pool_length" // Length of the pool.
)
// WorkoutCategory is category of workout
type WorkoutCategory int
// Workout Category
const (
WCWalk WorkoutCategory = 1
WCRun WorkoutCategory = 2
WCHiking WorkoutCategory = 3
WCSkating WorkoutCategory = 4
WCBMX WorkoutCategory = 5
WCBicycling WorkoutCategory = 6
WCSwimming WorkoutCategory = 7
WCSurfing WorkoutCategory = 8
WCKitesurfing WorkoutCategory = 9
WCWindsurfing WorkoutCategory = 10
WCBodyboard WorkoutCategory = 11
WCTennis WorkoutCategory = 12
WCTableTennis WorkoutCategory = 13
WCSquash WorkoutCategory = 14
WCBadminton WorkoutCategory = 15
WCLiftWeights WorkoutCategory = 16
WCCalisthenics WorkoutCategory = 17
WCElliptical WorkoutCategory = 18
WCPilates WorkoutCategory = 19
WCBasketBall WorkoutCategory = 20
WCSoccer WorkoutCategory = 21
WCFootball WorkoutCategory = 22
WCRugby WorkoutCategory = 23
WCVolleyBall WorkoutCategory = 24
WCWaterpolo WorkoutCategory = 25
WCHorseRiding WorkoutCategory = 26
WCGolf WorkoutCategory = 27
WCYoga WorkoutCategory = 28
WCDancing WorkoutCategory = 29
WCBoxing WorkoutCategory = 30
WCFencing WorkoutCategory = 31
WCWrestling WorkoutCategory = 32
WCMartialArts WorkoutCategory = 33
WCSkiing WorkoutCategory = 34
WCSnowboarding WorkoutCategory = 35
WCOther WorkoutCategory = 36
WCNoActivity WorkoutCategory = 128
WCRowing WorkoutCategory = 187
WCZumba WorkoutCategory = 188
WCBaseball WorkoutCategory = 191
WCHandball WorkoutCategory = 192
WCHockey WorkoutCategory = 193
WCIceHockey WorkoutCategory = 194
WCClimbing WorkoutCategory = 195
WCIceSkating WorkoutCategory = 196
WCMultiSport WorkoutCategory = 272
WCIndoorRunning WorkoutCategory = 307
WCIndoorCycling WorkoutCategory = 308
)
// SleepType is Sleep Type.
type SleepType string
// Sleep Type
const (
HrSleep SleepType = "hr" // Heart Rate.
RrSleep SleepType = "rr" // Respiration Rate.
SnoringSleep SleepType = "snoring" // Total snoring time.
)
// SleepSummariesType is Sleep Summaries Type.
type SleepSummariesType string
// Sleep Summaries Types.
const (
SSBdi SleepSummariesType = "breathing_disturbances_intensity" // Intensity of breathing disturbances
SSDsd SleepSummariesType = "deepsleepduration" // Duration in state deep sleep (in seconds).
SSD2s SleepSummariesType = "durationtosleep" // Time to sleep (in seconds).
SSD2w SleepSummariesType = "durationtowakeup" // Time to wake up (in seconds).
SSHrAvr SleepSummariesType = "hr_average" // Average heart rate.
SSHrMax SleepSummariesType = "hr_max" // Maximal heart rate.
SSHrMin SleepSummariesType = "hr_min" // Minimal heart rate.
SSLsd SleepSummariesType = "lightsleepduration" // Duration in state light sleep (in seconds).
SSRsd SleepSummariesType = "remsleepduration" // Duration in state REM sleep (in seconds).
SSRRAvr SleepSummariesType = "rr_average" // Average respiration rate.
SSRRMax SleepSummariesType = "rr_max" // Maximal respiration rate.
SSRRMin SleepSummariesType = "rr_min" // Minimal respiration rate.
SSSS SleepSummariesType = "sleep_score" // Sleep score
SSSng SleepSummariesType = "snoring" // Total snoring time
SSSngEC SleepSummariesType = "snoringepisodecount" // Numbers of snoring episodes of at least one minute
SSWupC SleepSummariesType = "wakeupcount" // Number of times the user woke up.
SSWupD SleepSummariesType = "wakeupduration" // Time spent awake (in seconds).
)
// SleepState is Sleep state
type SleepState int
// Sleep state
const (
Awake SleepState = 0
LightSleep SleepState = 1
DeepSleep SleepState = 2
REM SleepState = 3
)

16
go.mod Normal file
View File

@ -0,0 +1,16 @@
module git.siteop.biz/withings/withings
go 1.19
require (
github.com/pkg/errors v0.9.1
golang.org/x/oauth2 v0.5.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/golang/protobuf v1.5.2 // indirect
golang.org/x/net v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
)

29
go.sum Normal file
View File

@ -0,0 +1,29 @@
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

364
measure.go Normal file
View File

@ -0,0 +1,364 @@
package withings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
// FormParam is for http request parameter.
type FormParam struct {
key string
value string
}
// OffsetBase is used to check whether lastupdate or startdate/enddate is used in GetMeas
var OffsetBase time.Time = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
func createRequest(ctx context.Context, fp []FormParam, uri, method string) (*http.Request, error) {
form := url.Values{}
for _, v := range fp {
form.Add(v.key, v.value)
}
body := strings.NewReader(form.Encode())
u, err := url.ParseRequestURI(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, u.String(), body)
req = req.WithContext(ctx)
if err != nil {
return nil, err
}
return req, nil
}
func parseResponse(resp *http.Response, result interface{}) error {
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(rbody, result)
}
func reqAndParse(c *Client, fp []FormParam, url, method string, result interface{}) error {
ctx, cancel := getNewContext(c.Timeout)
defer cancel()
req, err := createRequest(ctx, fp, url, method)
if err != nil {
return err
}
resp, err := c.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
err = parseResponse(resp, result)
if err != nil {
return err
}
return nil
}
func createDataFields(fields interface{}) (string, error) {
df := []string{}
rv := reflect.ValueOf(fields)
switch rv.Kind() {
case reflect.Slice, reflect.Array:
for i := 0; i < rv.Len(); i++ {
irv := rv.Index(i)
switch irv.Kind() {
case reflect.String:
df = append(df, irv.String())
case reflect.Int:
df = append(df, fmt.Sprintf("%d", irv.Int()))
default:
return "", errors.Errorf("createDataFields allows string or int values.")
}
}
default:
return "", errors.Errorf("createDataFields Slice or Array.")
}
return strings.Join(df, ","), nil
}
// GetMeas call withings API Measure - GetMeas. (https://developer.withings.com/oauth2/#operation/measure-getmeas)
// cattype: category, 1 for real measures, 2 for user objectives
// startdate, enddate: Measure's start date, end date.
// lastupdate : Timestamp for requesting data that were updated or created after this date. Use this instead of startdate+endate.
// If lastupdate is set to a timestamp other than Offsetbase, getMeas will use lastupdate in preference to startdate/enddate.
// offset: When a first call retuns more:1 and offset:XX, set value XX in this parameter to retrieve next available rows.
// isOldToNew: If true, results must be sorted by oldest to newest. If false, results must be sorted by newest to oldest.
// isSerialized: if true, results must be parsed to Measurement.SerializedData
// mtype: Measurement Type. Set the measurement type you want to get data. See MeasType in enum.go.
func (c *Client) GetMeas(cattype CatType, startdate, enddate, lastupdate time.Time, offset int, isOldToNew, isSerialized bool, mtype ...MeasType) (*Measurement, error) {
if len(mtype) == 0 {
return nil, errors.Errorf("Need least one param as MeasType.")
}
mym := new(Measurement)
df, err := createDataFields(mtype)
if err != nil {
return nil, err
}
fp := []FormParam{
{PPaction, MeasureA},
{PPmeastype, df},
{PPcategory, fmt.Sprintf("%d", cattype)},
}
if offset != 0 {
fp = append(fp, FormParam{PPoffset, fmt.Sprintf("%d", offset)})
}
// The below sentence comes from Withings API document.
// > Timestamp for requesting data that were updated or created after this date.
// > Useful for data synchronization between systems.
// > Use this instead of startdate + enddate.
if lastupdate != OffsetBase {
fp = append(fp, FormParam{PPlastupdate, strconv.FormatInt(lastupdate.Unix(), 10)})
} else {
fp = append(fp, FormParam{PPstartdate, strconv.FormatInt(startdate.Unix(), 10)})
fp = append(fp, FormParam{PPenddate, strconv.FormatInt(enddate.Unix(), 10)})
}
err = reqAndParse(c, fp, c.MeasureURL, http.MethodPost, mym)
if err != nil {
return nil, err
}
if isOldToNew {
sort.Slice(mym.Body.Measuregrps, func(i, j int) bool {
return time.Unix(int64(mym.Body.Measuregrps[i].Date), 0).Before(time.Unix(int64(mym.Body.Measuregrps[j].Date), 0))
})
} else {
sort.Slice(mym.Body.Measuregrps, func(i, j int) bool {
return time.Unix(int64(mym.Body.Measuregrps[i].Date), 0).After(time.Unix(int64(mym.Body.Measuregrps[j].Date), 0))
})
}
if isSerialized {
mym.SerializedData, err = SerialMeas(mym)
if err != nil {
mym.SerializedData = nil
return mym, err
}
}
return mym, nil
}
// SerialMeas will parse measurement results.
func SerialMeas(mym *Measurement) (*SerialzedMeas, error) {
sm := new(SerialzedMeas)
for _, mGrp := range mym.Body.Measuregrps {
for _, meas := range mGrp.Measures {
val := MeasureData{
GrpID: mGrp.GrpID,
Date: time.Unix(int64(mGrp.Date), 0),
Value: float64(meas.Value) * math.Pow10(meas.Unit),
Attrib: mGrp.Attrib,
Category: mGrp.Category,
DeviceID: mGrp.DeviceID,
}
switch meas.Type {
case int(Weight):
sm.Weights = append(sm.Weights, val)
case int(Height):
sm.Heights = append(sm.Heights, val)
case int(FatFreeMass):
sm.FatFreeMass = append(sm.FatFreeMass, val)
case int(FatRatio):
sm.FatRatios = append(sm.FatRatios, val)
case int(FatMassWeight):
sm.FatMassWeights = append(sm.FatMassWeights, val)
case int(DiastolicBP):
sm.DiastolicBPs = append(sm.DiastolicBPs, val)
case int(SystolicBP):
sm.SystolicBPs = append(sm.SystolicBPs, val)
case int(HeartPulse):
sm.HeartPulses = append(sm.HeartPulses, val)
case int(Temp):
sm.Temps = append(sm.Temps, val)
case int(SPO2):
sm.SPO2s = append(sm.SPO2s, val)
case int(BodyTemp):
sm.BodyTemps = append(sm.BodyTemps, val)
case int(SkinTemp):
sm.SkinTemps = append(sm.SkinTemps, val)
case int(MuscleMass):
sm.MuscleMasses = append(sm.MuscleMasses, val)
case int(Hydration):
sm.Hydration = append(sm.Hydration, val)
case int(BoneMass):
sm.BoneMasses = append(sm.BoneMasses, val)
case int(PWaveVel):
sm.PWaveVel = append(sm.PWaveVel, val)
case int(VO2):
sm.VO2s = append(sm.VO2s, val)
default:
sm.UnknowVals = append(sm.UnknowVals, val)
}
}
}
return sm, nil
}
// GetActivity call withings API Measure v2 - Getactivity. (https://developer.withings.com/oauth2/#operation/measurev2-getactivity)
// startdate/enddate: Activity result start date, end date.
// lastupdate : Timestamp for requesting data that were updated or created after this date. Use this instead of startdate+endate.
// If lastupdate is set to a timestamp other than Offsetbase, getMeas will use lastupdate in preference to startdate/enddate.
// offset: When a first call retuns more:1 and offset:XX, set value XX in this parameter to retrieve next available rows.
// atype: Acitivity Type. Set the activity type you want to get data. See ActivityType in enum.go.
func (c *Client) GetActivity(startdate, enddate string, lastupdate int, offset int, atype ...ActivityType) (*Activities, error) {
if len(atype) == 0 {
return nil, errors.Errorf("Need least one param as ActivityType.")
}
act := new(Activities)
df, err := createDataFields(atype)
var fp []FormParam = []FormParam{
{PPaction, ActivityA},
{PPoffset, fmt.Sprintf("%d", offset)},
{PPdataFields, df},
}
if startdate == "" || enddate == "" {
fp = append(fp, FormParam{PPlastupdate, fmt.Sprintf("%d", lastupdate)})
} else {
fp = append(fp, FormParam{PPstartdateymd, startdate}, FormParam{PPenddateymd, enddate})
}
err = reqAndParse(c, fp, c.MeasureURLv2, http.MethodPost, act)
if err != nil {
return nil, err
}
return act, nil
}
// GetWorkouts call withings API Measure v2 - Getworkouts. (https://developer.withings.com/api-reference#operation/measurev2-getworkouts)
// startdate/enddate: Workouts result start date, end date.
// lastupdate : Timestamp for requesting data that were updated or created after this date. Use this instead of startdate+endate.
// If lastupdate is set to a timestamp other than Offsetbase, GetWorkouts will use lastupdate in preference to startdate/enddate.
// offset: When a first call retuns more:1 and offset:XX, set value XX in this parameter to retrieve next available rows.
// wtype: Workout Type. Set the workout type you want to get data. See WorkoutType in enum.go.
func (c *Client) GetWorkouts(startdate, enddate string, lastupdate int, offset int, wtype ...WorkoutType) (*Workouts, error) {
if len(wtype) == 0 {
return nil, errors.Errorf("Need least one param as WorkoutType.")
}
workouts := new(Workouts)
df, err := createDataFields(wtype)
var fp []FormParam = []FormParam{
{PPaction, WorkoutsA},
{PPoffset, fmt.Sprintf("%d", offset)},
{PPdataFields, df},
}
if startdate == "" || enddate == "" {
fp = append(fp, FormParam{PPlastupdate, fmt.Sprintf("%d", lastupdate)})
} else {
fp = append(fp, FormParam{PPstartdateymd, startdate}, FormParam{PPenddateymd, enddate})
}
err = reqAndParse(c, fp, c.MeasureURLv2, http.MethodPost, workouts)
if err != nil {
return nil, err
}
return workouts, nil
}
// GetSleep cal withings API Sleep v2 - Get. (https://developer.withings.com/oauth2/#operation/sleepv2-get)
// startdate/enddate: Measures' start date, end date.
// stype: Sleep Type. Set the sleep type you want to get data. See SleepType in enum.go.
func (c *Client) GetSleep(startdate, enddate time.Time, stype ...SleepType) (*Sleeps, error) {
if len(stype) == 0 {
return nil, errors.Errorf("Need least one param as SleepType.")
}
df, err := createDataFields(stype)
if err != nil {
return nil, err
}
var fp []FormParam = []FormParam{
{PPaction, SleepA},
{PPstartdate, strconv.FormatInt(startdate.Unix(), 10)},
{PPenddate, strconv.FormatInt(enddate.Unix(), 10)},
{PPdataFields, df},
}
slp := new(Sleeps)
err = reqAndParse(c, fp, c.SleepURLv2, http.MethodPost, slp)
if err != nil {
return nil, err
}
// sort by startdate
sort.Slice(slp.Body.Series, func(i, j int) bool {
return slp.Body.Series[i].Startdate < slp.Body.Series[j].Startdate
})
return slp, nil
}
// GetSleepSummary call withings API Sleep v2 - Getsummary. (https://developer.withings.com/oauth2/#operation/sleepv2-getsummary)
// startdate/enddate: Measurement result start date, end date.
// lastupdate : Timestamp for requesting data that were updated or created after this date. Use this instead of startdate+endate.
// If lastupdate is set to a timestamp other than Offsetbase, getMeas will use lastupdate in preference to startdate/enddate.
// stype: Sleep Summaries Type. Set the sleep summaries data you want to get. See SleepSummariesType in enum.go.
func (c *Client) GetSleepSummary(startdate, enddate string, lastupdate int, sstype ...SleepSummariesType) (*SleepSummaries, error) {
if len(sstype) == 0 {
return nil, errors.Errorf("Need least one param as SleepSummariesType.")
}
df, err := createDataFields(sstype)
if err != nil {
return nil, err
}
var fp []FormParam = []FormParam{
{PPaction, SleepSA},
{PPdataFields, df},
}
if startdate == "" || enddate == "" {
fp = append(fp, FormParam{PPlastupdate, fmt.Sprintf("%d", lastupdate)})
} else {
fp = append(fp, FormParam{PPstartdateymd, startdate}, FormParam{PPenddateymd, enddate})
}
slpss := new(SleepSummaries)
err = reqAndParse(c, fp, c.SleepURLv2, http.MethodPost, slpss)
if err != nil {
return nil, err
}
// sort by startdate
sort.Slice(slpss.Body.Series, func(i, j int) bool {
return slpss.Body.Series[i].Startdate < slpss.Body.Series[j].Startdate
})
return slpss, nil
}

268
measure_test.go Normal file
View File

@ -0,0 +1,268 @@
package withings
import (
"fmt"
"io/ioutil"
"os"
"testing"
"time"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
)
const (
tokenFile = "access_token.json"
layout = "2006-01-02"
layout2 = "2006-01-02 15:04:05"
isnotify = false
)
const (
testSettingsFile = ".test_settings.yaml"
testMeasureFile = "sample_measure.json"
testActivityFile = "sample_activity.json"
testSleepFile = "sample_sleep.json"
)
var (
jst *time.Location
tnow time.Time
adayago time.Time
lastupdate time.Time
ed string
sd string
settings map[string]string
client *(Client)
)
func setupForTest(settingsFile string, t *testing.T) {
settings = ReadSettings(settingsFile)
authWithTokenFile(settings, t)
tokenFuncs()
// to get data from a day ago to now
jst = time.FixedZone("Asis/Tokyo", 9*60*60)
tnow = time.Now()
//adayago := t.Add(-96 * time.Hour)
adayago = tnow.Add(-24 * time.Hour)
ed = tnow.Format(layout)
sd = adayago.Format(layout)
//lastupdate = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
lastupdate = OffsetBase
//lastupdate = time.Date(2020, 12, 20, 0, 0, 0, 0, time.UTC)
}
func authWithTokenFile(settings map[string]string, t *testing.T) {
var err error
client, err = New(settings["cid"], settings["secret"], settings["redirect_url"])
if err != nil {
t.Errorf("Failed to create New client:%v", err)
return
}
if _, err := os.Open(tokenFile); err == nil {
_, err = client.ReadToken(tokenFile)
if err != nil {
t.Fatalf("Failed to read token file:%v", err)
return
}
} else {
t.Fatalf("authWithTokenFile needs token file.")
}
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!")
//}
//// Save Token if you need
//err = client.SaveToken(tokenFile)
//if err != nil {
// fmt.Println("Failed to RefreshToken")
// fmt.Println(err)
// return
//}
//client.PrintToken()
}
//func TestGetMeas(t *testing.T) {
// setupForTest(testSettingsFile, t)
// jsonBlob, err := ioutil.ReadFile(testMeasureFile)
// if err != nil {
// t.Fatalf("ioutil.ReadFile returns error(%v)", err)
// }
//
// ts := httptest.NewServer(http.HandlerFunc(
// func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Content-Type", "application/json")
// //d := TestStruct{"Bob", 18}
// //slcBob, _ := json.Marshal(d)
// //w.Write(slcBob)
// w.Write(jsonBlob)
// }))
// defer ts.Close()
//
// client.MeasureURL = ts.URL
// res, err := client.GetMeas(Real, adayago, tnow, lastupdate, 0, Weight, Height, FatFreeMass)
// if err != nil {
// t.Errorf("client.GetMeas returned error:%v", err)
// } else {
// fmt.Println(res)
// }
//}
func callCreateDataFields(correct string, mtype interface{}, t *testing.T) {
df, err := createDataFields(mtype)
if err != nil {
t.Errorf("createDataFields, got error:%v", err)
} else {
fmt.Println(df)
if df != correct {
t.Errorf("createDataFields, = %v, want %v", df, correct)
}
}
}
func TestCreateDataFieldsInt(t *testing.T) {
correct := "1,4,5,6,8,9,10,11,12,54,71,73,76,77,88,91,123"
mtype := [...]MeasType{Weight, Height, FatFreeMass, FatRatio, FatMassWeight, DiastolicBP, SystolicBP, HeartPulse, Temp, SPO2, BodyTemp, SkinTemp, MuscleMass, Hydration, BoneMass, PWaveVel, VO2}
fmt.Println(mtype)
callCreateDataFields(correct, mtype, t)
}
func TestCreateDataFieldsString(t *testing.T) {
correct := "steps,distance,elevation,soft,moderate,intense,active,calories,totalcalories,hr_average,hr_min,hr_max,hr_zone_0,hr_zone_1,hr_zone_2,hr_zone_3"
mtype := [...]ActivityType{Steps, Distance, Elevation, Soft, Moderate, Intense, Active, Calories, TotalCalories, HrAverage, HrMin, HrMax, HrZone0, HrZone1, HrZone2, HrZone3}
fmt.Println(mtype)
callCreateDataFields(correct, mtype, t)
}
func compareRequests(t *testing.T, wURL *url.URL, wMethod, wProto, wBody string, res *http.Request) bool {
if res.URL.String() != wURL.String() {
t.Errorf("createRequest URL = %s, want %s", res.URL.String(), wURL.String())
}
if res.Method != wMethod {
t.Errorf("createRequest = %s, want %s", res.Method, wMethod)
return false
}
if res.Proto != wProto {
t.Errorf("createRequest = %s, want %s", res.Proto, wProto)
return false
}
b, e := res.GetBody()
if e != nil {
t.Errorf("res.GetBody return error(%v)", e)
return false
} else {
bbuf, e := ioutil.ReadAll(b)
if e != nil {
t.Errorf("ioutil.ReadAll(b) return error(%v)", e)
return false
} else {
bstr := string(bbuf)
if bstr != wBody {
t.Errorf("createRequest = %s, want %s", bstr, wBody)
return false
} else {
return true
}
}
}
}
func TestCreateRequest(t *testing.T) {
URL := "https://example.com"
wBody := "action=getmeas&category=1&meastype=1%2C4%2C5%2C6%2C8%2C9%2C10%2C11%2C12%2C54%2C71%2C73%2C76%2C77%2C88%2C91%2C123"
wMethod := http.MethodPost
wURL, _ := url.ParseRequestURI(URL)
wProto := "HTTP/1.1"
df := "1,4,5,6,8,9,10,11,12,54,71,73,76,77,88,91,123"
fp := []FormParam{
{PPaction, MeasureA},
{PPmeastype, df},
{PPcategory, fmt.Sprintf("%d", Real)},
}
ctx, cancel := getNewContext(5)
defer cancel()
res, err := createRequest(ctx, fp, URL, http.MethodPost)
if err != nil {
t.Errorf("createRequest returns error(%v)", err)
} else {
b, e := httputil.DumpRequest(res, true)
if e != nil {
t.Errorf("DumpRequest returns error(%v)", e)
} else {
fmt.Println(string(b))
compareRequests(t, wURL, wMethod, wProto, wBody, res)
}
}
}
type TestStruct struct {
Name string `json:"name"`
Age int `json:"int"`
}
// TODO: add fail pattern
func TestParseResponse(t *testing.T) {
jsonBlob, err := ioutil.ReadFile(testMeasureFile)
if err != nil {
t.Fatalf("ioutil.ReadFile returns error(%v)", err)
}
ts := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(jsonBlob)
}))
defer ts.Close()
resp, err := http.Get(ts.URL)
if err != nil {
t.Errorf("http.Get returns error(%v)", err)
}
defer resp.Body.Close()
mym := new(Measurement)
if err == nil {
bytes, err := httputil.DumpResponse(resp, true)
if err != nil {
fmt.Println(err)
}
fmt.Printf("\tDumpResponse Results\n%s\n", string(bytes))
e := parseResponse(resp, mym)
if e != nil {
t.Errorf("parseResponse returns error(%v)", e)
}
} else {
t.Errorf("http.Get returns error(%v)", err)
fmt.Println(err)
}
}

33
sample_activity.json Normal file
View File

@ -0,0 +1,33 @@
{
"status":0,
"body":{
"activities":[
{
"steps":8454,
"calories":374.474,
"hr_average":72,
"hr_min":60,
"hr_max":117,
"deviceid":null,
"timezone":"Asia\/Tokyo",
"date":"2021-01-03",
"brand":18,
"is_tracker":true
},
{
"steps":10280,
"calories":467.076,
"hr_average":86,
"hr_min":59,
"hr_max":104,
"deviceid":null,
"timezone":"Asia\/Tokyo",
"date":"2021-01-04",
"brand":18,
"is_tracker":true
}
],
"more":false,
"offset":0
}
}

66
sample_measure.json Normal file
View File

@ -0,0 +1,66 @@
{
"status":0,
"body":{
"updatetime":1609766464,
"timezone":"Asia\/Tokyo",
"measuregrps":[
{
"grpid":1234567890,
"attrib":0,
"date":1609754636,
"created":1609754674,
"category":1,
"deviceid":"ky8zry4dk9ng7dzysa8kk2cyi7yfdwaziz6wszug",
"hash_deviceid":"ky8zry4dk9ng7dzysa8kk2cyi7yfdwaziz6wszug",
"measures":[
{
"value":81200,
"type":1,
"unit":-3,
"algo":3,
"fm":5
},
{
"value":2180,
"type":8,
"unit":-2,
"algo":3,
"fm":5
},
{
"value":5619,
"type":76,
"unit":-2,
"algo":3,
"fm":5
},
{
"value":4560,
"type":77,
"unit":-2,
"algo":3,
"fm":5
},
{
"value":320,
"type":88,
"unit":-2,
"algo":3,
"fm":5
},
{
"value":26847,
"type":6,
"unit":-3
},
{
"value":59400,
"type":5,
"unit":-3
}
],
"comment":null
}
]
}
}

162
sample_sleep.json Normal file
View File

@ -0,0 +1,162 @@
{
"status":0,
"body":{
"series":[
{
"startdate":1609603140,
"state":0,
"enddate":1609603260,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609603260,
"state":1,
"enddate":1609603740,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609615260,
"state":1,
"enddate":1609615860,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609615860,
"state":2,
"enddate":1609616100,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609616100,
"state":1,
"enddate":1609616820,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609616820,
"state":2,
"enddate":1609618020,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609618020,
"state":1,
"enddate":1609619040,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609619040,
"state":2,
"enddate":1609619460,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609619460,
"state":1,
"enddate":1609620480,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609620480,
"state":2,
"enddate":1609621080,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609621080,
"state":1,
"enddate":1609621980,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609621980,
"state":2,
"enddate":1609622580,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609603740,
"state":2,
"enddate":1609606920,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609622580,
"state":1,
"enddate":1609623900,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609623900,
"state":2,
"enddate":1609624380,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609606920,
"state":1,
"enddate":1609608420,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609608420,
"state":2,
"enddate":1609611000,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609611000,
"state":1,
"enddate":1609612020,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609612020,
"state":2,
"enddate":1609612200,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609612200,
"state":1,
"enddate":1609614780,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609614780,
"state":2,
"enddate":1609615140,
"model":"Activite Steel HR",
"model_id":55
},
{
"startdate":1609615140,
"state":1,
"enddate":1609615260,
"model":"Activite Steel HR",
"model_id":55
}
],
"model":16
}
}

206
types.go Normal file
View File

@ -0,0 +1,206 @@
package withings
import "time"
// MeasureData is used for parsed Measurement.
type MeasureData struct {
GrpID int64
Date time.Time
Value float64
Attrib int
Category int
DeviceID string
}
// SerialzedMeas has parsed Measurements.
type SerialzedMeas struct {
Weights []MeasureData
Heights []MeasureData
FatFreeMass []MeasureData
FatRatios []MeasureData
FatMassWeights []MeasureData
DiastolicBPs []MeasureData
SystolicBPs []MeasureData
HeartPulses []MeasureData
Temps []MeasureData
SPO2s []MeasureData
BodyTemps []MeasureData
SkinTemps []MeasureData
MuscleMasses []MeasureData
Hydration []MeasureData
BoneMasses []MeasureData
PWaveVel []MeasureData
VO2s []MeasureData
UnknowVals []MeasureData
}
// Measurement is raw data from Measure API.
// See https://developer.withings.com/oauth2/#operation/measure-getmeas .
type Measurement struct {
Status int `json:"status"`
Body struct {
Updatetime int `json:"updatetime"`
Timezone string `json:"timezone"`
Measuregrps []struct {
GrpID int64 `json:"grpid"`
Attrib int `json:"attrib"`
Date int `json:"date"`
Created int `json:"created"`
Category int `json:"category"`
DeviceID string `json:"deviceid"`
HashDeviceID string `json:"hash_deviceid"`
Measures []struct {
Value int `json:"value"`
Type int `json:"type"`
Unit int `json:"unit"`
Algo int `json:"algo"`
Fm int `json:"fm"`
} `json:"measures"`
Comment string `json:"comment"`
} `json:"measuregrps"`
More int `json:"more"`
Offset int `json:"offset"`
} `json:"body"`
SerializedData *SerialzedMeas
}
// Activities is raw data from Measure Activity API.
// See https://developer.withings.com/oauth2/#operation/measurev2-getactivity .
type Activities struct {
Status int `json:"status"`
Body struct {
Activities []struct {
Date string `json:"date"`
Timezone string `json:"timezone"`
Deviceid string `json:"deviceid"`
Brand int `json:"brand"`
IsTracker bool `json:"is_tracker"`
Steps int `json:"steps"`
Distance int `json:"distance"`
Elevation int `json:"elevation"`
Soft int `json:"soft"`
Moderate int `json:"moderate"`
Intense int `json:"intense"`
Active int `json:"active"`
Calories float64 `json:"calories"`
Totalcalories int `json:"totalcalories"`
HrAverage int `json:"hr_average"`
HrMin int `json:"hr_min"`
HrMax int `json:"hr_max"`
HrZone0 int `json:"hr_zone_0"`
HrZone1 int `json:"hr_zone_1"`
HrZone2 int `json:"hr_zone_2"`
HrZone3 int `json:"hr_zone_3"`
} `json:"activities"`
More bool `json:"more"`
Offset int `json:"offset"`
} `json:"body"`
}
// Workouts is raw data from Measure Getworkouts API.
// See https://developer.withings.com/api-reference#operation/measurev2-getworkouts .
type Workouts struct {
Status int `json:"status"`
Body struct {
Series []struct {
ID int64 `json:"id"`
Category WorkoutCategory `json:"category"`
Timezone string `json:"timezone"`
Model int `json:"model"`
Attrib int `json:"attrib"`
Startdate int64 `json:"startdate"`
Enddate int64 `json:"enddate"`
Date string `json:"date"`
Modified int64 `json:"modified"`
DeviceID string `json:"deviceid"`
Data struct {
AlgoPauseDuration int `json:"algo_pause_duration"`
Calories float64 `json:"calories"`
Distance float64 `json:"distance"`
Effduration int `json:"effduration"`
Elevation int `json:"elevation"`
HrAverage int `json:"hr_average"`
HrMax int `json:"hr_max"`
HrMin int `json:"hr_min"`
HrZone0 int `json:"hr_zone_0"`
HrZone1 int `json:"hr_zone_1"`
HrZone2 int `json:"hr_zone_2"`
HrZone3 int `json:"hr_zone_3"`
Intensity int `json:"intensity"`
ManualCalories int `json:"manual_calories"`
ManualDistance int `json:"manual_distance"`
PauseDuration int `json:"pause_duration"`
PoolLaps int `json:"pool_laps"`
PoolLength int `json:"pool_length"`
Spo2Average int `json:"spo2_average"`
Steps int `json:"steps"`
Strokes int `json:"strokes"`
} `json:"data"`
} `json:"series"`
More bool `json:"more"`
Offset int `json:"offset"`
} `json:"body"`
}
// Sleeps is raw data from Sleep API.
// See https://developer.withings.com/oauth2/#tag/sleep .
type Sleeps struct {
Status int `json:"status"`
Body struct {
Series []struct {
Startdate int64 `json:"startdate"`
Enddate int64 `json:"enddate"`
State int `json:"state"`
Hr struct {
Timestamp int `json:"timestamp"`
} `json:"hr"`
Rr struct {
Timestamp int `json:"timestamp"`
} `json:"rr"`
Snoring struct {
Timestamp int `json:"timestamp"`
} `json:"snoring"`
} `json:"series"`
Model int `json:"model"`
ModelID int `json:"model_id"`
} `json:"body"`
}
// SleepSummaries is raw data from Sleep Summaries API.
// See https://developer.withings.com/oauth2/#operation/sleepv2-getsummary .
type SleepSummaries struct {
Status int `json:"status"`
Body struct {
Series []struct {
Timezone string `json:"timezone"`
Model int `json:"model"`
ModelID int `json:"model_id"`
Startdate int64 `json:"startdate"`
Enddate int64 `json:"enddate"`
Date string `json:"date"`
Created int64 `json:"created"`
Modified int64 `json:"modified"`
Data struct {
BreathingDisturbancesIntensity int `json:"breathing_disturbances_intensity"`
Deepsleepduration int `json:"deepsleepduration"`
Durationtosleep int `json:"durationtosleep"`
Durationtowakeup int `json:"durationtowakeup"`
HrAverage int `json:"hr_average"`
HrMax int `json:"hr_max"`
HrMin int `json:"hr_min"`
Lightsleepduration int `json:"lightsleepduration"`
Remsleepduration int `json:"remsleepduration"`
RrAverage int `json:"rr_average"`
RrMax int `json:"rr_max"`
RrMin int `json:"rr_min"`
SleepScore int `json:"sleep_score"`
Snoring int `json:"snoring"`
Snoringepisodecount int `json:"snoringepisodecount"`
Wakeupcount int `json:"wakeupcount"`
Wakeupduration int `json:"wakeupduration"`
} `json:"data"`
} `json:"series"`
More bool `json:"more"`
Offset int `json:"offset"`
} `json:"body"`
}