Package fitbit: Implement Sleep() to read sleep logs.
authorFlorian Forster <ff@octo.it>
Wed, 31 Jan 2018 15:44:31 +0000 (16:44 +0100)
committerFlorian Forster <ff@octo.it>
Wed, 31 Jan 2018 15:44:31 +0000 (16:44 +0100)
fitbit/sleep.go [new file with mode: 0644]
fitbit/sleep_test.go [new file with mode: 0644]

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)
+}
diff --git a/fitbit/sleep_test.go b/fitbit/sleep_test.go
new file mode 100644 (file)
index 0000000..5d83858
--- /dev/null
@@ -0,0 +1,116 @@
+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)
+       }
+}