--- /dev/null
+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)
+}
--- /dev/null
+package fitbit
+
+import (
+ "context"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestParseSleep(t *testing.T) {
+ ctx := context.Background()
+
+ input := `{
+ "dateOfSleep": "2017-04-02",
+ "duration": 42000,
+ "efficiency": 42,
+ "isMainSleep": true,
+ "levels": {
+ "summary": {
+ "deep": {
+ "count": 42,
+ "minutes": 42,
+ "thirtyDayAvgMinutes": 42
+ },
+ "light": {
+ "count": 42,
+ "minutes": 42,
+ "thirtyDayAvgMinutes": 42
+ },
+ "rem": {
+ "count": 42,
+ "minutes": 42,
+ "thirtyDayAvgMinutes": 42
+ },
+ "wake": {
+ "count": 42,
+ "minutes": 42,
+ "thirtyDayAvgMinutes": 42
+ }
+ },
+ "data": [
+ {
+ "datetime": "2017-04-01T23:58:30.000",
+ "level": "wake",
+ "seconds": 1080
+ },
+ {
+ "datetime": "2017-04-02T00:16:30.000",
+ "level": "rem",
+ "seconds": 2000
+ }
+ ],
+ "shortData": [
+ {
+ "datetime": "2017-04-01T23:58:30.000",
+ "level": "wake",
+ "seconds": 1080
+ },
+ {
+ "datetime": "2017-04-02T00:16:30.000",
+ "level": "deep",
+ "seconds": 100
+ },
+ {
+ "datetime": "2017-04-02T00:18:10.000",
+ "level": "rem",
+ "seconds": 1900
+ }
+ ]
+ },
+ "logId": 42,
+ "minutesAfterWakeup": 42,
+ "minutesAsleep": 42,
+ "minutesAwake": 42,
+ "minutesToFallAsleep": 0,
+ "startTime": "2017-04-01T23:58:30.000",
+ "timeInBed": 42,
+ "type": "stages"
+}`
+
+ want := &Sleep{
+ Stages: []SleepStage{
+ SleepStage{
+ StartTime: time.Date(2017, time.April, 1, 23, 58, 30, 0, time.UTC),
+ EndTime: time.Date(2017, time.April, 2, 0, 16, 30, 0, time.UTC),
+ Level: SleepLevelWake,
+ },
+ SleepStage{
+ StartTime: time.Date(2017, time.April, 2, 0, 16, 30, 0, time.UTC),
+ EndTime: time.Date(2017, time.April, 2, 0, 18, 10, 0, time.UTC),
+ Level: SleepLevelDeep,
+ },
+ SleepStage{
+ StartTime: time.Date(2017, time.April, 2, 0, 18, 10, 0, time.UTC),
+ EndTime: time.Date(2017, time.April, 2, 0, 49, 50, 0, time.UTC),
+ Level: SleepLevelREM,
+ },
+ },
+ }
+
+ got, err := parseSleep(ctx, []byte(input), time.UTC)
+ if err != nil {
+ t.Errorf("parseSleep() = %v", err)
+ }
+
+ for i, stg := range got.Stages {
+ t.Logf("got.Stages[%d] = %+v", i, stg)
+ }
+ for i, stg := range want.Stages {
+ t.Logf("want.Stages[%d] = %+v", i, stg)
+ }
+
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("parseSleep() = %+v, want %+v", got, want)
+ }
+}