Package fitbit: Implement Sleep() to read sleep logs.
[kraftakt.git] / fitbit / sleep.go
diff --git a/fitbit/sleep.go b/fitbit/sleep.go
new file mode 100644 (file)
index 0000000..76fc197
--- /dev/null
@@ -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)
+}