Skip to content

Backfill uses update_time as the weigh-in time, so bulk server-side updates collapse years of history onto one date #56

Description

@randalotto

Summary

_parse_record takes the measurement timestamp from the Eufy record's update_time. That field
reflects when the record was last modified server-side, not when the weigh-in happened. Eufy
appears to rewrite it in bulk on some accounts, which makes --backfill-days file the entire
history under a single date.

Forward syncing is unaffected — for a fresh weigh-in create_time and update_time are a couple
of seconds apart — so this only shows up when backfilling.

Version

eufy-sync 1.8.1 (PyPI), Python 3.12, Linux.

The line

eufy_sync/eufy_client.py:385

update_time = record.get("update_time", record.get("create_time", 0))

The create_time fallback is effectively dead code: update_time is always present in the
/device/data response, so dict.get's default never fires.

Evidence

On my account, /device/data returns 548 records:

Field Distinct values Range
create_time 544 2021-03-04 → 2026-08-02
update_time 116 2021-03-04 → 2026-08-02

The update_time values are heavily clustered: 318 of the 319 records for my profile share a
single ~2½-minute window on 2026-07-05. I don't know what triggered it server-side — an app
migration or a re-link of the scale would be my guess — but the effect is that
--backfill-days 3650 proposed 318 measurements all stamped that one afternoon:

[DRY RUN] Would sync to garmin: 92.6 kg at 2026-07-05 19:02:30+00:00
[DRY RUN] Would sync to garmin: 93.3 kg at 2026-07-05 19:02:30+00:00
[DRY RUN] Would sync to garmin: 96.4 kg at 2026-07-05 19:02:30+00:00
...
[DRY RUN] Would sync 319 measurements to Garmin.

create_time on those same records is spread naturally across five years, which matches my
actual weigh-in history.

Steps to reproduce

Reproducing needs an account whose records have been bulk-touched, so the general check is:

  1. Fetch the raw records (EufyClient._get_records(None)).
  2. Compare len({r["create_time"] for r in recs}) against
    len({r["update_time"] for r in recs}).
  3. If the update_time set is much smaller, --dry-run --backfill-days 3650 will show the
    collapsed dates.

Impact

Anyone backfilling on an affected account writes their whole history to Garmin Connect under one
date. Bulk-deleting body composition entries from Garmin Connect is painful, so this is
unpleasant to recover from — worth catching before it lands.

Because it only manifests on backfill, an affected user can easily not notice until the data is
already in Garmin.

Suggested fix

Prefer create_time, keep update_time as the fallback:

update_time = record.get("create_time") or record.get("update_time", 0)

I've been running this locally as a build-time patch and the resulting backfill dates line up with
my real history.

A cheap safety net worth considering alongside it: have --dry-run/backfill warn when a large
number of measurements share one timestamp, since that's a strong signal the date field is wrong
rather than the data being genuinely that dense.

Happy to open a PR if the one-liner is the direction you'd want.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions