package fitbit import ( "context" "encoding/json" "fmt" "io/ioutil" "time" "google.golang.org/appengine/log" ) // SleepLevel is the depth of sleep. // // The Fitbit API provides these in one of two ways. The newer representation // is ("deep", "light", "rem" and "wake"), the previous (older) representation // is ("asleep", "restless" and "awake"). We map the old representation to the // newer one, because we'd need to do that for Google Fit anyway. This is done // like so: // // asleep → deep // restless → light // awake → wake type SleepLevel int const ( SleepLevelUnknown SleepLevel = iota SleepLevelDeep SleepLevelLight SleepLevelREM SleepLevelWake ) // SleepStage is one stage during a user's sleep. type SleepStage struct { StartTime time.Time EndTime time.Time Level SleepLevel } // Sleep is one period of sleep that is broken down into multiple stages. type Sleep struct { Stages []SleepStage } func parseSleep(ctx context.Context, data []byte, loc *time.Location) (*Sleep, error) { type rawData struct { DateTime string Level string Seconds int64 } var jsonSleep struct { Levels struct { Data []rawData ShortData []rawData } } if err := json.Unmarshal(data, &jsonSleep); err != nil { return nil, err } rawStages := jsonSleep.Levels.Data if len(jsonSleep.Levels.ShortData) != 0 { rawStages = jsonSleep.Levels.ShortData } var ret Sleep for _, stg := range rawStages { tm, err := time.ParseInLocation("2006-01-02T15:04:05.999", stg.DateTime, loc) if err != nil { log.Warningf(ctx, "unable to parse time: %q", stg.DateTime) continue } var level SleepLevel switch stg.Level { case "deep", "asleep": level = SleepLevelDeep case "light", "restless": level = SleepLevelLight case "rem": level = SleepLevelREM case "wake", "awake": level = SleepLevelWake default: log.Warningf(ctx, "unknown sleep level: %q", stg.Level) continue } ret.Stages = append(ret.Stages, SleepStage{ StartTime: tm, EndTime: tm.Add(time.Duration(stg.Seconds) * time.Second), Level: level, }) } if len(ret.Stages) == 0 && len(jsonSleep.Levels.Data) != 0 { return nil, fmt.Errorf("parsing sleep stages failed") } return &ret, nil } // Sleep returns the sleep log for date t. Times are parsed in the same timezone as t. func (c *Client) Sleep(ctx context.Context, t time.Time) (*Sleep, error) { url := fmt.Sprintf("https://api.fitbit.com/1.2/user/%s/sleep/date/%s.json", c.fitbitUserID, t.Format("2006-01-02")) res, err := c.client.Get(url) if err != nil { return nil, err } defer res.Body.Close() data, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } log.Debugf(ctx, "GET %s -> %s", url, data) return parseSleep(ctx, data, t.Location()) }