Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions internal/cmd/sleep.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ var sleepDayCmd = &cobra.Command{
if date == "" {
date = time.Now().Format("2006-01-02")
}
tz := viper.GetString("timezone")
if tz == "local" {
tz = time.Local.String()
}
tz := resolveTimezone(viper.GetString("timezone"))
cl := client.New(viper.GetString("email"), viper.GetString("password"), viper.GetString("user_id"), viper.GetString("client_id"), viper.GetString("client_secret"))
day, err := cl.GetSleepDay(context.Background(), date, tz)
if err != nil {
Expand Down
5 changes: 1 addition & 4 deletions internal/cmd/sleep_range.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ var sleepRangeCmd = &cobra.Command{
if end.Before(start) {
return fmt.Errorf("to must be >= from")
}
tz := viper.GetString("timezone")
if tz == "local" {
tz = time.Local.String()
}
tz := resolveTimezone(viper.GetString("timezone"))
cl := client.New(viper.GetString("email"), viper.GetString("password"), viper.GetString("user_id"), viper.GetString("client_id"), viper.GetString("client_secret"))
rows := []map[string]any{}
for d := start; !d.After(end); d = d.Add(24 * time.Hour) {
Expand Down
51 changes: 51 additions & 0 deletions internal/cmd/timezone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"os"
"path/filepath"
"runtime"
"strings"
)

// resolveTimezone converts a timezone string to a valid IANA timezone name.
// Go's time.Local.String() returns "Local" which is not a valid IANA name
// and is rejected by the Eight Sleep API.
func resolveTimezone(tz string) string {
if tz != "" && tz != "local" && tz != "Local" {
return tz
}
if iana := localIANA(); iana != "" {
return iana
}
return tz
}

// localIANA detects the IANA timezone name from the operating system.
func localIANA() string {
if tz := os.Getenv("TZ"); tz != "" && tz != "Local" {
return tz
}
switch runtime.GOOS {
case "darwin":
// macOS: /etc/localtime is a symlink into zoneinfo
target, err := os.Readlink("/etc/localtime")
if err == nil {
if idx := strings.Index(target, "zoneinfo/"); idx >= 0 {
return target[idx+len("zoneinfo/"):]
}
}
case "linux":
if b, err := os.ReadFile("/etc/timezone"); err == nil {
if tz := strings.TrimSpace(string(b)); tz != "" {
return tz
}
}
target, err := filepath.EvalSymlinks("/etc/localtime")
if err == nil {
if idx := strings.Index(target, "zoneinfo/"); idx >= 0 {
return target[idx+len("zoneinfo/"):]
}
}
}
return ""
}