package fitbit import ( "context" "encoding/json" "fmt" "io/ioutil" "sort" "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 } type byTime []SleepStage func (t byTime) Len() int { return len(t) } func (t byTime) Less(i, j int) bool { return t[i].StartTime.Before(t[j].StartTime) } func (t byTime) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func parseSleep(ctx context.Context, data []byte, loc *time.Location) (*Sleep, error) { type rawData struct { DateTime string Level string Seconds int64 } var parsed struct { Sleep []struct { Levels struct { Data []rawData ShortData []rawData } } } if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } var allStages []rawData for _, s := range parsed.Sleep { if len(s.Levels.Data) != 0 { allStages = append(allStages, s.Levels.Data...) } if len(s.Levels.ShortData) != 0 { allStages = append(allStages, s.Levels.ShortData...) } } var ret Sleep for _, stg := range allStages { 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(allStages) != 0 { return nil, fmt.Errorf("parsing sleep stages failed") } sort.Sort(byTime(ret.Stages)) 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()) }