From 6cbd58ab03c5146d701d2a0d6d26428a79eb3463 Mon Sep 17 00:00:00 2001 From: Terastar-Paperclip <279373186+Terastar-Paperclip@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:40:45 -0500 Subject: [PATCH 1/4] feat: expose resolved date window --- cmd/activity.go | 3 ++ cmd/intraday.go | 3 ++ cmd/measurements.go | 11 +++--- cmd/resolved_window.go | 22 ++++++++++++ cmd/sleep.go | 5 ++- cmd/workouts.go | 75 +++++++++++++++++++++-------------------- resolved_window_test.go | 36 ++++++++++++++++++++ 7 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 cmd/resolved_window.go create mode 100644 resolved_window_test.go diff --git a/cmd/activity.go b/cmd/activity.go index a9b27d9..1244aec 100644 --- a/cmd/activity.go +++ b/cmd/activity.go @@ -63,6 +63,9 @@ var activityCmd = &cobra.Command{ if err != nil { return err } + if printed, err := printResolvedWindow(cmd, since, until.AddDate(0, 0, 1)); printed || err != nil { + return err + } dataFields := "steps,distance,elevation,soft,moderate,intense,active," + "calories,totalcalories,hr_average,hr_min,hr_max,hr_zone_0,hr_zone_1,hr_zone_2,hr_zone_3" diff --git a/cmd/intraday.go b/cmd/intraday.go index 103a7f7..db2c3c3 100644 --- a/cmd/intraday.go +++ b/cmd/intraday.go @@ -83,6 +83,9 @@ Default window is the last 24h — intraday is dense; wider ranges are slow.`, if err != nil { return err } + if printed, err := printResolvedWindow(cmd, since, until); printed || err != nil { + return err + } dataFields := "steps,elevation,calories,distance,duration,heart_rate," + "hrv_quality,rmssd,sdnn1,spo2_auto" diff --git a/cmd/measurements.go b/cmd/measurements.go index 4faafe7..f7a0893 100644 --- a/cmd/measurements.go +++ b/cmd/measurements.go @@ -54,10 +54,10 @@ type measure struct { } type getmeasResponse struct { - Timezone string `json:"timezone"` - Measuregrps []measureGroup `json:"measuregrps"` - More int `json:"more"` - Offset int `json:"offset"` + Timezone string `json:"timezone"` + Measuregrps []measureGroup `json:"measuregrps"` + More int `json:"more"` + Offset int `json:"offset"` } type measurementRow struct { @@ -94,6 +94,9 @@ var measurementsCmd = &cobra.Command{ if err != nil { return err } + if printed, err := printResolvedWindow(cmd, since, until); printed || err != nil { + return err + } params := url.Values{} params.Set("action", "getmeas") diff --git a/cmd/resolved_window.go b/cmd/resolved_window.go new file mode 100644 index 0000000..4bc211e --- /dev/null +++ b/cmd/resolved_window.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "time" +) + +func init() { + for _, cmd := range []*cobra.Command{measurementsCmd, intradayCmd, sleepCmd, activityCmd, workoutsCmd} { + cmd.Flags().Bool("print-resolved-window", false, "Print resolved dates without contacting Withings") + } +} + +func printResolvedWindow(cmd *cobra.Command, since, until time.Time) (bool, error) { + enabled, err := cmd.Flags().GetBool("print-resolved-window") + if err != nil || !enabled { + return false, err + } + _, err = fmt.Fprintf(cmd.OutOrStdout(), "since=%s until_exclusive=%s\n", since.Format(time.RFC3339), until.Format(time.RFC3339)) + return true, err +} diff --git a/cmd/sleep.go b/cmd/sleep.go index f23e91a..2f56e79 100644 --- a/cmd/sleep.go +++ b/cmd/sleep.go @@ -58,6 +58,9 @@ var sleepCmd = &cobra.Command{ if err != nil { return err } + if printed, err := printResolvedWindow(cmd, since, until.AddDate(0, 0, 1)); printed || err != nil { + return err + } // All sleep summary data fields we care about. Must be explicitly requested. dataFields := "wakeupduration,lightsleepduration,deepsleepduration,remsleepduration," + @@ -244,7 +247,7 @@ func deriveSleep(c *client.Client, date time.Time) (*sleepSeries, error) { // heart rate, or a meaningful burst of steps. Small step samples (bathroom // trips, rolling over) do not break the run — they're just HealthKit's way // of reporting brief motion without an accompanying HR reading. - const hrBreak = 90 // bpm above this = awake/active + const hrBreak = 90 // bpm above this = awake/active const stepsBreak = 30 const maxGap int64 = 90 * 60 // 90 min — tolerate watch-off-for-charging diff --git a/cmd/workouts.go b/cmd/workouts.go index e465e1a..bd37f4a 100644 --- a/cmd/workouts.go +++ b/cmd/workouts.go @@ -16,42 +16,42 @@ import ( // Withings workout category codes. Common ones only — unknowns are rendered as numbers. var workoutCategoryNames = map[int]string{ - 1: "walk", - 2: "run", - 3: "hiking", - 4: "skating", - 5: "bmx", - 6: "bicycling", - 7: "swimming", - 8: "surfing", - 9: "kitesurfing", - 10: "windsurfing", - 11: "bodyboard", - 12: "tennis", - 13: "tabletennis", - 14: "squash", - 15: "badminton", - 16: "lift_weights", - 17: "calisthenics", - 18: "elliptical", - 19: "pilates", - 20: "basketball", - 21: "soccer", - 22: "football", - 23: "rugby", - 24: "volleyball", - 25: "waterpolo", - 26: "horse_riding", - 27: "golf", - 28: "yoga", - 29: "dancing", - 30: "boxing", - 31: "fencing", - 32: "wrestling", - 33: "martial_arts", - 34: "skiing", - 35: "snowboarding", - 36: "other", + 1: "walk", + 2: "run", + 3: "hiking", + 4: "skating", + 5: "bmx", + 6: "bicycling", + 7: "swimming", + 8: "surfing", + 9: "kitesurfing", + 10: "windsurfing", + 11: "bodyboard", + 12: "tennis", + 13: "tabletennis", + 14: "squash", + 15: "badminton", + 16: "lift_weights", + 17: "calisthenics", + 18: "elliptical", + 19: "pilates", + 20: "basketball", + 21: "soccer", + 22: "football", + 23: "rugby", + 24: "volleyball", + 25: "waterpolo", + 26: "horse_riding", + 27: "golf", + 28: "yoga", + 29: "dancing", + 30: "boxing", + 31: "fencing", + 32: "wrestling", + 33: "martial_arts", + 34: "skiing", + 35: "snowboarding", + 36: "other", 128: "rowing", 187: "zumba", 188: "baseball", @@ -109,6 +109,9 @@ var workoutsCmd = &cobra.Command{ if err != nil { return err } + if printed, err := printResolvedWindow(cmd, since, until.AddDate(0, 0, 1)); printed || err != nil { + return err + } dataFields := "calories,effduration,intensity,manual_distance,manual_calories," + "hr_average,hr_min,hr_max,hr_zone_0,hr_zone_1,hr_zone_2,hr_zone_3," + diff --git a/resolved_window_test.go b/resolved_window_test.go new file mode 100644 index 0000000..97945e2 --- /dev/null +++ b/resolved_window_test.go @@ -0,0 +1,36 @@ +package main_test + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestResolvedWindowOffline(t *testing.T) { + bin := filepath.Join(t.TempDir(), "export-cli") + build := exec.Command("go", "build", "-o", bin, ".") + if out, err := build.CombinedOutput(); err != nil { + t.Fatalf("build: %v: %s", err, out) + } + cases := []struct{ zone, day, want string }{ + {"Pacific/Honolulu", "2026-04-15", "since=2026-04-15T00:00:00-10:00 until_exclusive=2026-04-16T00:00:00-10:00\n"}, + {"Pacific/Kiritimati", "2026-04-15", "since=2026-04-15T00:00:00+14:00 until_exclusive=2026-04-16T00:00:00+14:00\n"}, + {"America/New_York", "2026-03-08", "since=2026-03-08T00:00:00-05:00 until_exclusive=2026-03-09T00:00:00-04:00\n"}, + {"America/New_York", "2026-11-01", "since=2026-11-01T00:00:00-04:00 until_exclusive=2026-11-02T00:00:00-05:00\n"}, + } + for _, sub := range []string{"activity", "intraday", "measurements", "sleep", "workouts"} { + for _, tc := range cases { + t.Run(sub+"/"+tc.zone+"/"+tc.day, func(t *testing.T) { + args := append(strings.Fields(sub), "--since", tc.day, "--until", tc.day, "--print-resolved-window") + cmd := exec.Command(bin, args...) + cmd.Env = []string{"TZ=" + tc.zone, "HOME=" + t.TempDir(), "HTTPS_PROXY=http://127.0.0.1:1", "PATH=" + os.Getenv("PATH")} + out, err := cmd.CombinedOutput() + if err != nil || string(out) != tc.want { + t.Fatalf("output=%q error=%v, want %q", out, err, tc.want) + } + }) + } + } +} From 4b0948797c34fda886b6ba96d50d76cabaec2513 Mon Sep 17 00:00:00 2001 From: Terastar-Paperclip <279373186+Terastar-Paperclip@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:42:09 -0500 Subject: [PATCH 2/4] test: run resolved-window compatibility checks --- compat_contract_test.go | 10 ++++++++++ go.mod | 2 +- go.sum | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/compat_contract_test.go b/compat_contract_test.go index 8a73ccd..c22c736 100644 --- a/compat_contract_test.go +++ b/compat_contract_test.go @@ -21,6 +21,7 @@ import ( "time" "github.com/quantcli/common/compat" + "github.com/quantcli/common/compat/dates" "github.com/quantcli/common/compat/formats" ) @@ -112,3 +113,12 @@ func writeFakeAuthToken(t *testing.T) string { } return home } + +// TestResolvedWindow verifies local dates independently of upstream credentials. +func TestResolvedWindow(t *testing.T) { + bin := os.Getenv("WITHINGS_EXPORT_BIN") + if bin == "" { + t.Skip("WITHINGS_EXPORT_BIN not set") + } + dates.RunResolvedWindow(t, compat.Runner{Binary: bin, Subcommands: []string{"activity", "intraday", "measurements", "sleep", "workouts"}}) +} diff --git a/go.mod b/go.mod index 62ffa38..7df2a0b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/quantcli/withings-export-cli go 1.26.10 require ( - github.com/quantcli/common/compat v0.0.0-20260511001927-62c4c6634ff5 + github.com/quantcli/common/compat v0.0.0-20261109234045-9d8d6519f80d github.com/spf13/cobra v1.10.2 ) diff --git a/go.sum b/go.sum index ed5f5d2..a3439da 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/quantcli/common/compat v0.0.0-20260511001927-62c4c6634ff5 h1:NoWh40ITAC6W7YyIp2eM6RyRKLHzXcs8TXUjwoYrlUY= github.com/quantcli/common/compat v0.0.0-20260511001927-62c4c6634ff5/go.mod h1:VBC/zEphSZgCZS1rhWsR3A8EWYSbTkP/MwqWHL7266s= +github.com/quantcli/common/compat v0.0.0-20261109234045-9d8d6519f80d h1:ni/BNzhb2xyGYckZOEzk4bNih6HaukiJ11JP7ubDAXU= +github.com/quantcli/common/compat v0.0.0-20261109234045-9d8d6519f80d/go.mod h1:VBC/zEphSZgCZS1rhWsR3A8EWYSbTkP/MwqWHL7266s= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= From dec929bb809f1ddf58ff826fbca2f3b9841f6175 Mon Sep 17 00:00:00 2001 From: Terastar-Paperclip <279373186+Terastar-Paperclip@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:43:00 -0500 Subject: [PATCH 3/4] ci: run resolved window compatibility tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b63eb1..d9dd935 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,4 +44,4 @@ jobs: env: # Path the compat-tagged test reads from os.Getenv. WITHINGS_EXPORT_BIN: /tmp/withings-export - run: go test -tags=compat -run TestContractFormats ./... + run: go test -tags=compat -run 'TestContractFormats|TestResolvedWindow' ./... From 1a1cbbd04f25300a206593ad05585b8f09664616 Mon Sep 17 00:00:00 2001 From: Terastar-Paperclip <279373186+Terastar-Paperclip@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:35:42 -0500 Subject: [PATCH 4/4] build: use merged date compatibility checks --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 7df2a0b..7008325 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/quantcli/withings-export-cli go 1.26.10 require ( - github.com/quantcli/common/compat v0.0.0-20261109234045-9d8d6519f80d + github.com/quantcli/common/compat v0.0.0-20261110001356-0b66a42dfa85 github.com/spf13/cobra v1.10.2 ) diff --git a/go.sum b/go.sum index a3439da..691839b 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/quantcli/common/compat v0.0.0-20260511001927-62c4c6634ff5 h1:NoWh40ITAC6W7YyIp2eM6RyRKLHzXcs8TXUjwoYrlUY= -github.com/quantcli/common/compat v0.0.0-20260511001927-62c4c6634ff5/go.mod h1:VBC/zEphSZgCZS1rhWsR3A8EWYSbTkP/MwqWHL7266s= -github.com/quantcli/common/compat v0.0.0-20261109234045-9d8d6519f80d h1:ni/BNzhb2xyGYckZOEzk4bNih6HaukiJ11JP7ubDAXU= -github.com/quantcli/common/compat v0.0.0-20261109234045-9d8d6519f80d/go.mod h1:VBC/zEphSZgCZS1rhWsR3A8EWYSbTkP/MwqWHL7266s= +github.com/quantcli/common/compat v0.0.0-20261110001356-0b66a42dfa85 h1:NxUSVjE+3clP9Ss6uvlwZ0SToE7kmpkioRJ5r8ZUmK4= +github.com/quantcli/common/compat v0.0.0-20261110001356-0b66a42dfa85/go.mod h1:VBC/zEphSZgCZS1rhWsR3A8EWYSbTkP/MwqWHL7266s= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=