Skip to content

Backfill dates from create_time and add --repair-days; 1.9.0 - #59

Merged
sturimcode merged 3 commits into
mainfrom
repair-and-create-time
Aug 14, 2026
Merged

Backfill dates from create_time and add --repair-days; 1.9.0#59
sturimcode merged 3 commits into
mainfrom
repair-and-create-time

Conversation

@sturimcode

Copy link
Copy Markdown
Owner

Two fixes that meet in the same place: getting the right dates onto Garmin, and getting them there when the local record disagrees with the target.

Weigh-in time comes from create_time

_parse_record read the timestamp from update_time, which is when Eufy last touched the record server-side, not when the weigh-in happened. Eufy rewrites that field in bulk on some accounts (one report: 318 of 319 records sharing a single timestamp), so --backfill-days filed years of history under one date. Forward syncing never showed it, because on a fresh weigh-in the two fields are seconds apart. The old create_time fallback was dead code: update_time is always present in the /device/data response, so the get() default never fired.

Both the timestamp and the measurement id now come from create_time, falling back to update_time when it is missing or zero. Ids are stable across Eufy's rewrites as a result. On an account that already synced under rewritten ids the old state rows go stale, and a backfill or repair re-uploads with the correct dates; Garmin de-dupes by timestamp, so that is safe. The raw Wi-Fi endpoint is untouched, since it carries its own timestamp field.

Fixes #56.

--repair-days N

Local state can say a measurement was delivered while the target no longer has it: entries deleted by hand in Garmin Connect, or dates the bug above filed wrong and the user cleaned up. Nothing could re-send those, because every path keyed off is_synced.

--repair-days N sets the fetch window the way --backfill-days does, then ignores is_synced when deciding whether to upload. The state lookup stays, because it still decides whether the sync is recorded: re-uploading a known id must not insert a second row past the UNIQUE constraint. Passing both flags is rejected, since they set the same window from opposite intents.

What repair leaves alone:

  • The weight-only upgrade still replaces the Garmin entry rather than adding one beside it (Raw Wi-Fi weight-only record can duplicate or block the later full body-comp record #48).
  • The same-date guard still skips a date another source owns in Garmin. Our own deleted dates pass it, because we have a sync row for them.
  • Strava receives only the newest measurement in the window. It stores a single current weight, so replaying history there would spend the rate limit (100 non-upload calls per 15 minutes) to land the same value.

One related fix fell out of this: the skip-because-already-in-Garmin row is now written only when there is no row yet. Repair reaches that branch a second time for the same measurement, and the insert would have hit the UNIQUE constraint.

Closes #58.

Also

README documents the flag and the recovery path, including the part the tool cannot do for you: wrong-dated entries have to be deleted in Garmin Connect first, since eufy-sync never deletes data it did not just replace. Version bumped to 1.9.0 in both places.

345 tests, 13 new: create_time priority and its three fallbacks, the fetch window selecting on weigh-in time, and repair mode against an already-synced record, a weight-only row, a foreign same-date entry, an already-skipped date, Strava, dry run, and the two CLI paths.

update_time is when Eufy last touched the record server-side, not when
the weigh-in happened. Eufy rewrites it in bulk on some accounts (one
report: 318 of 319 records sharing a single timestamp), so a backfill
filed years of history under one date. create_time survives those
rewrites, and the old create_time fallback was dead code: update_time is
always present in the /device/data response, so the get() default never
fired.

Both the measurement timestamp and the measurement id now come from
create_time, falling back to update_time when create_time is missing or
zero. Ids are therefore stable across Eufy's rewrites. On an account that
already synced under rewritten ids the old state rows go stale, and a
backfill re-uploads with the correct dates; Garmin de-dupes by timestamp,
so that is safe. The raw Wi-Fi endpoint is untouched: it carries its own
timestamp field and never had this problem.
Local state can say a measurement was delivered while the target no
longer has it: entries deleted by hand in Garmin Connect, or dates that
the update_time bug filed wrong and the user cleaned up. Nothing could
re-send those, because every path keyed off is_synced.

--repair-days N sets the fetch window like --backfill-days, then ignores
is_synced when deciding whether to upload. The state lookup itself stays,
because it still decides whether to record the sync: re-uploading a known
id must not insert a second row past the UNIQUE constraint. The two flags
are rejected together, since they set the same window from opposite
intents.

What repair does not change:
- The weight-only upgrade still replaces the Garmin entry rather than
  adding one beside it (issue #48).
- The same-date guard still skips a date another source owns in Garmin.
  Our own deleted dates pass it, because we have a sync row for them.
- Strava receives only the newest measurement in the window. It stores a
  single current weight, so replaying history there would spend the rate
  limit (100 non-upload calls per 15 minutes) to land the same value.

The skip-because-already-in-Garmin row is now written only when there is
no row yet. Repair reaches that branch a second time for the same
measurement, and the insert would have hit the UNIQUE constraint.
The recovery path needs its own paragraph: --backfill-days looks like the
flag for this and is not, since it trusts the local record.
@sturimcode
sturimcode merged commit de40248 into main Aug 14, 2026
6 checks passed
@sturimcode
sturimcode deleted the repair-and-create-time branch August 14, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant