X-Git-Url: https://git.octo.it/?p=kraftakt.git;a=blobdiff_plain;f=fitbit%2Fsleep.go;fp=fitbit%2Fsleep.go;h=76fc197d528171bc213d12b32d50ac63b9bb80c2;hp=0000000000000000000000000000000000000000;hb=0760c07303a8b8a78a4858e7331d3189f3d83af3;hpb=0b5456de50f3e76354cb3222fa16a7dfd5b066bf diff --git a/fitbit/sleep.go b/fitbit/sleep.go new file mode 100644 index 0000000..76fc197 --- /dev/null +++ b/fitbit/sleep.go @@ -0,0 +1,122 @@ +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. Times are parsed in the user's timeozne, loc. +func (c *Client) Sleep(ctx context.Context, date string, loc *time.Location) (*Sleep, error) { + url := fmt.Sprintf("https://api.fitbit.com/1.2/user/%s/sleep/date/%s.json", + c.fitbitUserID, date) + + 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, loc) +}