init
This commit is contained in:
commit
6bd83d373a
|
@ -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
|
||||
}
|
|
@ -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 body’s 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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
}
|
Loading…
Reference in New Issue