From e031e2211c5756a5ac48d17f429a718694f6efeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 9 Sep 2026 22:47:05 +0200 Subject: [PATCH] fix(import): honour the energy unit an archived workout carries `totalEnergyBurned` rides the same account-display-unit contract as the distance beside it: a Health app set to kilojoules writes `kJ`, and the column is kilocalories. Convert it through the shared table. Refs #944 --- .../import-apple-health-export.test.ts | 32 +++++++++++++++++++ .../import-apple-health-export.ts | 14 ++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/lib/measurements/__tests__/import-apple-health-export.test.ts b/src/lib/measurements/__tests__/import-apple-health-export.test.ts index c63637ec9..607a332b2 100644 --- a/src/lib/measurements/__tests__/import-apple-health-export.test.ts +++ b/src/lib/measurements/__tests__/import-apple-health-export.test.ts @@ -1297,6 +1297,38 @@ describe("streamParseExportXml — the record's own unit (issue #944)", () => { expect(prisma._measurements[0].value).toBeCloseTo(2484, 6); }); + it("stores a kilojoule workout burn in kilocalories", async () => { + const tmp = mkdtempSync(join(tmpdir(), "healthlog-parser-energy-test-")); + const xmlPath = join(tmp, "export.xml"); + writeFileSync( + xmlPath, + ` + + +`, + ); + const prisma = makeFakePrisma(); + await streamParseExportXml({ + xmlPath, + userId: "user-energy", + userTimezone: "Europe/Berlin", + prisma: prisma as unknown as PrismaClient, + }); + + expect(prisma._workouts).toHaveLength(1); + expect(prisma._workouts[0].totalDistanceM).toBeCloseTo(6500, 6); + expect(prisma._workouts[0].totalEnergyKcal).toBeCloseTo(412.0458891, 6); + }); + it("stores a mile-scale walking distance in metres", async () => { const { prisma } = await importCumulativeFixture( cumulativeExportXml( diff --git a/src/lib/measurements/import-apple-health-export.ts b/src/lib/measurements/import-apple-health-export.ts index 4a4bb9229..ea14ad8fe 100644 --- a/src/lib/measurements/import-apple-health-export.ts +++ b/src/lib/measurements/import-apple-health-export.ts @@ -42,7 +42,10 @@ import { } from "@/lib/measurements/drain-per-sample-cumulative"; import { reconcileExternalMeasurement } from "@/lib/measurements/reconcile-external-measurement"; import { resolveHkWorkoutSportType } from "@/lib/measurements/hk-workout-activity-type-map"; -import { hkDistanceToMetres } from "@/lib/measurements/hk-units"; +import { + convertHkValue, + hkDistanceToMetres, +} from "@/lib/measurements/hk-units"; import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit"; import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger"; import { emitDataArrival } from "@/lib/arrivals/emit-shared"; @@ -948,6 +951,7 @@ export async function streamParseExportXml( const totalDistance = Number.parseFloat(attrs.totalDistance ?? ""); const totalEnergy = Number.parseFloat(attrs.totalEnergyBurned ?? ""); const distanceUnit = attrs.totalDistanceUnit ?? ""; + const energyUnit = attrs.totalEnergyBurnedUnit ?? ""; // Apple ships the ACCOUNT's own length unit on HKWorkout.totalDistance // (km for a metric account, mi for an imperial one); metres is the // canonical DB unit. issue #944 — the same conversion the `` @@ -969,7 +973,13 @@ export async function streamParseExportXml( startedAt: startDate, endedAt: endDate, durationSec, - totalEnergyKcal: Number.isFinite(totalEnergy) ? totalEnergy : null, + totalEnergyKcal: Number.isFinite(totalEnergy) + ? // issue #944 — the energy attribute carries the account's own + // unit too: a Health app set to kilojoules writes `kJ` here, and + // the column is kilocalories. Same shared conversion; an unknown + // unit leaves the number alone. + (convertHkValue(totalEnergy, energyUnit, "kcal") ?? totalEnergy) + : null, totalDistanceM: distanceM, externalId, externalSourceVersion: attrs.sourceVersion ?? null,