@kaiord/fit 7.1.1 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +532 -43
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sportSchema, durationTypeSchema, createConsoleLogger, createFitParsingError,
|
|
1
|
+
import { fileTypeSchema, sportSchema, durationTypeSchema, createConsoleLogger, createFitParsingError, workoutSchema, isRepetitionBlock, convertLengthToMeters, subSportSchema, lengthUnitSchema, equipmentSchema, targetTypeSchema, FIT_TO_SWIM_STROKE, targetUnitSchema, intensitySchema } from '@kaiord/core';
|
|
2
2
|
import { Stream, Decoder, Encoder, Profile } from '@garmin/fitsdk';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
@@ -8,8 +8,7 @@ import { z } from 'zod';
|
|
|
8
8
|
var FIT_MESSAGE_NUMBERS = {
|
|
9
9
|
FILE_ID: 0,
|
|
10
10
|
WORKOUT: 26,
|
|
11
|
-
WORKOUT_STEP: 27
|
|
12
|
-
};
|
|
11
|
+
WORKOUT_STEP: 27};
|
|
13
12
|
z.enum([
|
|
14
13
|
"device",
|
|
15
14
|
"settings",
|
|
@@ -195,8 +194,6 @@ var mapSubSportToFit = (krdSubSport) => {
|
|
|
195
194
|
}
|
|
196
195
|
return KRD_TO_FIT_SUB_SPORT_MAP[result.data] || fitSubSportSchema.enum.generic;
|
|
197
196
|
};
|
|
198
|
-
|
|
199
|
-
// src/adapters/krd-to-fit/krd-to-fit-metadata.mapper.ts
|
|
200
197
|
var DEFAULT_MANUFACTURER = "garmin";
|
|
201
198
|
var mapManufacturer = (manufacturer, logger) => {
|
|
202
199
|
if (!manufacturer) {
|
|
@@ -215,25 +212,44 @@ var mapManufacturer = (manufacturer, logger) => {
|
|
|
215
212
|
);
|
|
216
213
|
return DEFAULT_MANUFACTURER;
|
|
217
214
|
};
|
|
215
|
+
var countValidSteps = (steps) => {
|
|
216
|
+
let count = 0;
|
|
217
|
+
for (const step of steps) {
|
|
218
|
+
if (isRepetitionBlock(step)) {
|
|
219
|
+
count += countValidSteps(step.steps) + 1;
|
|
220
|
+
} else {
|
|
221
|
+
count += 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return count;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/adapters/krd-to-fit/krd-to-fit-metadata.converter.ts
|
|
228
|
+
var resolveFitFileType = (krdType) => {
|
|
229
|
+
if (krdType === "structured_workout") return "workout";
|
|
230
|
+
if (krdType === "recorded_activity") return "activity";
|
|
231
|
+
if (krdType === "course") return "course";
|
|
232
|
+
return "workout";
|
|
233
|
+
};
|
|
234
|
+
var assignParsedNumber = (target, key, raw) => {
|
|
235
|
+
const parsed = parseInt(raw, 10);
|
|
236
|
+
if (!isNaN(parsed)) {
|
|
237
|
+
target[key] = parsed;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
218
240
|
var convertMetadataToFileId = (krd, logger) => {
|
|
219
241
|
logger.debug("Converting metadata to file_id message");
|
|
220
|
-
const fileType = krd.type
|
|
242
|
+
const fileType = resolveFitFileType(krd.type);
|
|
221
243
|
const fileId = {
|
|
222
|
-
type: FIT_FILE_TYPE_TO_NUMBER[fileType]
|
|
244
|
+
type: FIT_FILE_TYPE_TO_NUMBER[fileType],
|
|
223
245
|
timeCreated: new Date(krd.metadata.created),
|
|
224
246
|
manufacturer: mapManufacturer(krd.metadata.manufacturer, logger)
|
|
225
247
|
};
|
|
226
248
|
if (krd.metadata.product !== void 0) {
|
|
227
|
-
|
|
228
|
-
if (!isNaN(productNumber)) {
|
|
229
|
-
fileId.product = productNumber;
|
|
230
|
-
}
|
|
249
|
+
assignParsedNumber(fileId, "product", krd.metadata.product);
|
|
231
250
|
}
|
|
232
251
|
if (krd.metadata.serialNumber) {
|
|
233
|
-
|
|
234
|
-
if (!isNaN(serialNumber)) {
|
|
235
|
-
fileId.serialNumber = serialNumber;
|
|
236
|
-
}
|
|
252
|
+
assignParsedNumber(fileId, "serialNumber", krd.metadata.serialNumber);
|
|
237
253
|
}
|
|
238
254
|
return fileId;
|
|
239
255
|
};
|
|
@@ -254,17 +270,6 @@ var convertWorkoutMetadata = (workout, logger) => {
|
|
|
254
270
|
}
|
|
255
271
|
return workoutMesg;
|
|
256
272
|
};
|
|
257
|
-
var countValidSteps = (steps) => {
|
|
258
|
-
let count = 0;
|
|
259
|
-
for (const step of steps) {
|
|
260
|
-
if (isRepetitionBlock(step)) {
|
|
261
|
-
count += step.steps.length + 1;
|
|
262
|
-
} else {
|
|
263
|
-
count += 1;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return count;
|
|
267
|
-
};
|
|
268
273
|
var fitDurationTypeSchema = z.enum([
|
|
269
274
|
"time",
|
|
270
275
|
"distance",
|
|
@@ -490,7 +495,7 @@ var convertStrokeTarget = (step, message) => {
|
|
|
490
495
|
message.targetValue = strokeValue.value;
|
|
491
496
|
};
|
|
492
497
|
|
|
493
|
-
// src/adapters/krd-to-fit/krd-to-fit-target.
|
|
498
|
+
// src/adapters/krd-to-fit/krd-to-fit-target.converter.ts
|
|
494
499
|
var convertTarget = (step, message) => {
|
|
495
500
|
if (step.target.type === targetTypeSchema.enum.open) {
|
|
496
501
|
message.targetType = fitTargetTypeSchema.enum.open;
|
|
@@ -671,9 +676,468 @@ var fitMessageKeySchema = z.enum([
|
|
|
671
676
|
"sessionMesgs",
|
|
672
677
|
"recordMesgs",
|
|
673
678
|
"eventMesgs",
|
|
674
|
-
"lapMesgs"
|
|
679
|
+
"lapMesgs",
|
|
680
|
+
// Health domain (KRD v2.0)
|
|
681
|
+
"sleepLevelMesgs",
|
|
682
|
+
"monitoringMesgs",
|
|
683
|
+
"monitoringInfoMesgs",
|
|
684
|
+
"weightScaleMesgs",
|
|
685
|
+
"bodyCompositionMesgs",
|
|
686
|
+
"hrvStatusSummaryMesgs",
|
|
687
|
+
"hrvValueMesgs",
|
|
688
|
+
"stressLevelMesgs"
|
|
675
689
|
]);
|
|
676
690
|
|
|
691
|
+
// src/adapters/health/shared/health-metadata.builder.ts
|
|
692
|
+
var isString = (value) => typeof value === "string";
|
|
693
|
+
var isNumber = (value) => typeof value === "number";
|
|
694
|
+
var MS_PER_S = 1e3;
|
|
695
|
+
var convertFitTimeCreatedToIso = (timeCreated) => {
|
|
696
|
+
if (timeCreated instanceof Date) return timeCreated.toISOString();
|
|
697
|
+
if (isNumber(timeCreated))
|
|
698
|
+
return new Date(timeCreated * MS_PER_S).toISOString();
|
|
699
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
700
|
+
};
|
|
701
|
+
var buildHealthMetadata = (fileId) => ({
|
|
702
|
+
created: convertFitTimeCreatedToIso(fileId?.timeCreated),
|
|
703
|
+
manufacturer: isString(fileId?.manufacturer) ? fileId.manufacturer : void 0,
|
|
704
|
+
product: isNumber(fileId?.product) ? String(fileId.product) : void 0
|
|
705
|
+
});
|
|
706
|
+
var fitBodyCompositionSchema = z.object({
|
|
707
|
+
timestamp: z.union([z.date(), z.string(), z.number()]),
|
|
708
|
+
percentFat: z.number().optional(),
|
|
709
|
+
percentHydration: z.number().optional(),
|
|
710
|
+
visceralFatRating: z.number().optional(),
|
|
711
|
+
boneMass: z.number().optional(),
|
|
712
|
+
muscleMass: z.number().optional(),
|
|
713
|
+
basalMet: z.number().optional(),
|
|
714
|
+
bmi: z.number().optional()
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// src/adapters/health/body-composition/health-body-composition.converter.ts
|
|
718
|
+
var HEALTH_VERSION = "2.0";
|
|
719
|
+
var toIsoString = (value) => {
|
|
720
|
+
if (value instanceof Date) return value.toISOString();
|
|
721
|
+
if (typeof value === "number") return new Date(value * 1e3).toISOString();
|
|
722
|
+
return new Date(value).toISOString();
|
|
723
|
+
};
|
|
724
|
+
var FIT_KG_SCALE = 100;
|
|
725
|
+
var fromScaledKg = (value) => value === void 0 ? void 0 : value / FIT_KG_SCALE;
|
|
726
|
+
var collectMeasurements = (fit) => {
|
|
727
|
+
const out = {};
|
|
728
|
+
if (fit.percentFat !== void 0) out.bodyFatPercent = fit.percentFat;
|
|
729
|
+
if (fit.percentHydration !== void 0)
|
|
730
|
+
out.bodyWaterPercent = fit.percentHydration;
|
|
731
|
+
const leanMassKilograms = fromScaledKg(fit.muscleMass);
|
|
732
|
+
if (leanMassKilograms !== void 0)
|
|
733
|
+
out.leanMassKilograms = leanMassKilograms;
|
|
734
|
+
const boneMassKilograms = fromScaledKg(fit.boneMass);
|
|
735
|
+
if (boneMassKilograms !== void 0)
|
|
736
|
+
out.boneMassKilograms = boneMassKilograms;
|
|
737
|
+
if (fit.bmi !== void 0) out.bmi = fit.bmi;
|
|
738
|
+
return out;
|
|
739
|
+
};
|
|
740
|
+
var mapFitBodyCompositionToKrd = (fit) => {
|
|
741
|
+
const measurements = collectMeasurements(fit);
|
|
742
|
+
if (Object.keys(measurements).length === 0) return void 0;
|
|
743
|
+
return {
|
|
744
|
+
kind: "bodyComposition",
|
|
745
|
+
version: HEALTH_VERSION,
|
|
746
|
+
measuredAt: toIsoString(fit.timestamp),
|
|
747
|
+
...measurements
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// src/adapters/health/body-composition/fit-to-krd-health-body-composition.converter.ts
|
|
752
|
+
var KRD_VERSION = "2.0";
|
|
753
|
+
var parseFirstBodyComposition = (raw, logger) => {
|
|
754
|
+
for (const entry of raw) {
|
|
755
|
+
const result = fitBodyCompositionSchema.safeParse(entry);
|
|
756
|
+
if (result.success) return result.data;
|
|
757
|
+
logger.warn("Skipping malformed body_composition message", {
|
|
758
|
+
issues: result.error.issues
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return void 0;
|
|
762
|
+
};
|
|
763
|
+
var convertFitToKrdHealthBodyComposition = (messages, logger) => {
|
|
764
|
+
const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
|
|
765
|
+
const rawBody = messages[fitMessageKeySchema.enum.bodyCompositionMesgs] ?? [];
|
|
766
|
+
const fitBody = parseFirstBodyComposition(rawBody, logger);
|
|
767
|
+
const body = fitBody ? mapFitBodyCompositionToKrd(fitBody) : void 0;
|
|
768
|
+
return {
|
|
769
|
+
version: KRD_VERSION,
|
|
770
|
+
type: fileTypeSchema.enum.body_composition,
|
|
771
|
+
metadata: buildHealthMetadata(fileId),
|
|
772
|
+
extensions: body ? { health: { bodyComposition: body } } : void 0
|
|
773
|
+
};
|
|
774
|
+
};
|
|
775
|
+
var fitMonitoringInfoSchema = z.object({
|
|
776
|
+
timestamp: z.union([z.date(), z.string(), z.number()]),
|
|
777
|
+
restingMetabolicRate: z.number().optional()
|
|
778
|
+
});
|
|
779
|
+
var fitMonitoringSchema = z.object({
|
|
780
|
+
timestamp: z.union([z.date(), z.string(), z.number()]).optional(),
|
|
781
|
+
steps: z.number().int().nonnegative().optional(),
|
|
782
|
+
activeCalories: z.number().int().nonnegative().optional(),
|
|
783
|
+
durationMin: z.number().int().nonnegative().optional(),
|
|
784
|
+
cycles: z.number().nonnegative().optional(),
|
|
785
|
+
distance: z.number().nonnegative().optional(),
|
|
786
|
+
activeTime: z.number().int().nonnegative().optional(),
|
|
787
|
+
activityType: z.string().optional()
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// src/adapters/health/daily/health-daily.converter.ts
|
|
791
|
+
var HEALTH_VERSION2 = "2.0";
|
|
792
|
+
var toIsoDate = (value) => {
|
|
793
|
+
if (!value) return void 0;
|
|
794
|
+
const date = value instanceof Date ? value : typeof value === "number" ? new Date(value * 1e3) : new Date(value);
|
|
795
|
+
return date.toISOString().slice(0, 10);
|
|
796
|
+
};
|
|
797
|
+
var sumSteps = (samples) => samples.reduce((sum, s) => sum + (s.steps ?? 0), 0);
|
|
798
|
+
var sumActiveCalories = (samples) => samples.reduce((sum, s) => sum + (s.activeCalories ?? 0), 0);
|
|
799
|
+
var pickDate = (info, samples) => {
|
|
800
|
+
if (info) return toIsoDate(info.timestamp);
|
|
801
|
+
const firstWithTs = samples.find((s) => s.timestamp);
|
|
802
|
+
return firstWithTs ? toIsoDate(firstWithTs.timestamp) : void 0;
|
|
803
|
+
};
|
|
804
|
+
var mapFitMonitoringToKrdDaily = (info, monitoring) => {
|
|
805
|
+
const date = pickDate(info, monitoring);
|
|
806
|
+
if (!date) return void 0;
|
|
807
|
+
return {
|
|
808
|
+
kind: "daily",
|
|
809
|
+
version: HEALTH_VERSION2,
|
|
810
|
+
date,
|
|
811
|
+
steps: sumSteps(monitoring),
|
|
812
|
+
activeCalories: sumActiveCalories(monitoring),
|
|
813
|
+
restingCalories: info?.restingMetabolicRate ?? 0,
|
|
814
|
+
intensityMinutes: { moderate: 0, vigorous: 0 }
|
|
815
|
+
};
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/adapters/health/daily/fit-to-krd-health-daily.converter.ts
|
|
819
|
+
var KRD_VERSION2 = "2.0";
|
|
820
|
+
var parseMonitoringInfo = (raw, logger) => {
|
|
821
|
+
for (const entry of raw) {
|
|
822
|
+
const result = fitMonitoringInfoSchema.safeParse(entry);
|
|
823
|
+
if (result.success) return result.data;
|
|
824
|
+
logger.warn("Skipping malformed monitoring_info message", {
|
|
825
|
+
issues: result.error.issues
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
return void 0;
|
|
829
|
+
};
|
|
830
|
+
var parseMonitoring = (raw, logger) => {
|
|
831
|
+
const parsed = [];
|
|
832
|
+
for (const entry of raw) {
|
|
833
|
+
const result = fitMonitoringSchema.safeParse(entry);
|
|
834
|
+
if (result.success) parsed.push(result.data);
|
|
835
|
+
else
|
|
836
|
+
logger.warn("Skipping malformed monitoring message", {
|
|
837
|
+
issues: result.error.issues
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
return parsed;
|
|
841
|
+
};
|
|
842
|
+
var convertFitToKrdHealthDaily = (messages, logger) => {
|
|
843
|
+
const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
|
|
844
|
+
const info = parseMonitoringInfo(
|
|
845
|
+
messages[fitMessageKeySchema.enum.monitoringInfoMesgs] ?? [],
|
|
846
|
+
logger
|
|
847
|
+
);
|
|
848
|
+
const monitoring = parseMonitoring(
|
|
849
|
+
messages[fitMessageKeySchema.enum.monitoringMesgs] ?? [],
|
|
850
|
+
logger
|
|
851
|
+
);
|
|
852
|
+
const daily = mapFitMonitoringToKrdDaily(info, monitoring);
|
|
853
|
+
return {
|
|
854
|
+
version: KRD_VERSION2,
|
|
855
|
+
type: fileTypeSchema.enum.daily_wellness,
|
|
856
|
+
metadata: buildHealthMetadata(fileId),
|
|
857
|
+
extensions: daily ? { health: { daily } } : void 0
|
|
858
|
+
};
|
|
859
|
+
};
|
|
860
|
+
var fitHrvStatusSummarySchema = z.object({
|
|
861
|
+
timestamp: z.union([z.date(), z.string(), z.number()]),
|
|
862
|
+
weeklyAverage: z.number().optional(),
|
|
863
|
+
lastNightAverage: z.number().optional(),
|
|
864
|
+
lastNight5MinHigh: z.number().optional(),
|
|
865
|
+
baselineLowUpper: z.number().optional(),
|
|
866
|
+
baselineBalancedLower: z.number().optional(),
|
|
867
|
+
baselineBalancedUpper: z.number().optional(),
|
|
868
|
+
status: z.enum(["none", "poor", "low", "unbalanced", "balanced"]).optional()
|
|
869
|
+
});
|
|
870
|
+
var fitHrvValueSchema = z.object({
|
|
871
|
+
timestamp: z.union([z.date(), z.string(), z.number()]),
|
|
872
|
+
value: z.number()
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// src/adapters/health/hrv/health-hrv.converter.ts
|
|
876
|
+
var HEALTH_VERSION3 = "2.0";
|
|
877
|
+
var toIsoString2 = (value) => {
|
|
878
|
+
if (value instanceof Date) return value.toISOString();
|
|
879
|
+
if (typeof value === "number") return new Date(value * 1e3).toISOString();
|
|
880
|
+
return new Date(value).toISOString();
|
|
881
|
+
};
|
|
882
|
+
var mapFitHrvToKrd = (summary, firstValue) => {
|
|
883
|
+
if (summary && summary.lastNightAverage !== void 0) {
|
|
884
|
+
return {
|
|
885
|
+
kind: "hrv",
|
|
886
|
+
version: HEALTH_VERSION3,
|
|
887
|
+
measuredAt: toIsoString2(summary.timestamp),
|
|
888
|
+
rMSSD: summary.lastNightAverage,
|
|
889
|
+
measurementWindow: "overnight"
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (firstValue) {
|
|
893
|
+
return {
|
|
894
|
+
kind: "hrv",
|
|
895
|
+
version: HEALTH_VERSION3,
|
|
896
|
+
measuredAt: toIsoString2(firstValue.timestamp),
|
|
897
|
+
rMSSD: firstValue.value,
|
|
898
|
+
measurementWindow: "spot"
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
return void 0;
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// src/adapters/health/hrv/fit-to-krd-health-hrv.converter.ts
|
|
905
|
+
var KRD_VERSION3 = "2.0";
|
|
906
|
+
var parseFirstSummary = (raw, logger) => {
|
|
907
|
+
for (const entry of raw) {
|
|
908
|
+
const result = fitHrvStatusSummarySchema.safeParse(entry);
|
|
909
|
+
if (result.success) return result.data;
|
|
910
|
+
logger.warn("Skipping malformed hrv_status_summary message", {
|
|
911
|
+
issues: result.error.issues
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return void 0;
|
|
915
|
+
};
|
|
916
|
+
var parseFirstValue = (raw, logger) => {
|
|
917
|
+
for (const entry of raw) {
|
|
918
|
+
const result = fitHrvValueSchema.safeParse(entry);
|
|
919
|
+
if (result.success) return result.data;
|
|
920
|
+
logger.warn("Skipping malformed hrv_value message", {
|
|
921
|
+
issues: result.error.issues
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
return void 0;
|
|
925
|
+
};
|
|
926
|
+
var convertFitToKrdHealthHrv = (messages, logger) => {
|
|
927
|
+
const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
|
|
928
|
+
const summary = parseFirstSummary(
|
|
929
|
+
messages[fitMessageKeySchema.enum.hrvStatusSummaryMesgs] ?? [],
|
|
930
|
+
logger
|
|
931
|
+
);
|
|
932
|
+
const firstValue = parseFirstValue(
|
|
933
|
+
messages[fitMessageKeySchema.enum.hrvValueMesgs] ?? [],
|
|
934
|
+
logger
|
|
935
|
+
);
|
|
936
|
+
const hrv = mapFitHrvToKrd(summary, firstValue);
|
|
937
|
+
return {
|
|
938
|
+
version: KRD_VERSION3,
|
|
939
|
+
type: fileTypeSchema.enum.hrv_summary,
|
|
940
|
+
metadata: buildHealthMetadata(fileId),
|
|
941
|
+
extensions: hrv ? { health: { hrv } } : void 0
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
var fitSleepLevelSchema = z.object({
|
|
945
|
+
timestamp: z.union([z.date(), z.string(), z.number()]),
|
|
946
|
+
sleepLevel: z.enum(["awake", "light", "deep", "rem", "unmeasurable"])
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// src/adapters/health/sleep/health-sleep.converter.ts
|
|
950
|
+
var HEALTH_VERSION4 = "2.0";
|
|
951
|
+
var toIsoString3 = (value) => {
|
|
952
|
+
if (value instanceof Date) return value.toISOString();
|
|
953
|
+
if (typeof value === "number") return new Date(value * 1e3).toISOString();
|
|
954
|
+
return new Date(value).toISOString();
|
|
955
|
+
};
|
|
956
|
+
var isKnownSleepStage = (level) => level === "awake" || level === "light" || level === "deep" || level === "rem";
|
|
957
|
+
var mapFitSleepLevelsToKrdSleep = (fitLevels) => {
|
|
958
|
+
if (fitLevels.length < 2) return void 0;
|
|
959
|
+
const sorted = [...fitLevels].sort(
|
|
960
|
+
(a, b) => new Date(toIsoString3(a.timestamp)).getTime() - new Date(toIsoString3(b.timestamp)).getTime()
|
|
961
|
+
);
|
|
962
|
+
const startTime = toIsoString3(sorted[0].timestamp);
|
|
963
|
+
const endTime = toIsoString3(sorted[sorted.length - 1].timestamp);
|
|
964
|
+
const stages = [];
|
|
965
|
+
for (let i = 0; i < sorted.length - 1; i += 1) {
|
|
966
|
+
const current = sorted[i];
|
|
967
|
+
if (!isKnownSleepStage(current.sleepLevel)) continue;
|
|
968
|
+
const startMs = new Date(toIsoString3(current.timestamp)).getTime();
|
|
969
|
+
const nextMs = new Date(toIsoString3(sorted[i + 1].timestamp)).getTime();
|
|
970
|
+
const durationSeconds = Math.max(0, Math.round((nextMs - startMs) / 1e3));
|
|
971
|
+
stages.push({
|
|
972
|
+
stage: current.sleepLevel,
|
|
973
|
+
startTime: toIsoString3(current.timestamp),
|
|
974
|
+
durationSeconds
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
const totalDurationSeconds = stages.reduce(
|
|
978
|
+
(sum, stage) => sum + stage.durationSeconds,
|
|
979
|
+
0
|
|
980
|
+
);
|
|
981
|
+
return {
|
|
982
|
+
kind: "sleep",
|
|
983
|
+
version: HEALTH_VERSION4,
|
|
984
|
+
startTime,
|
|
985
|
+
endTime,
|
|
986
|
+
totalDurationSeconds,
|
|
987
|
+
stages
|
|
988
|
+
};
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// src/adapters/health/sleep/fit-to-krd-health-sleep.converter.ts
|
|
992
|
+
var KRD_VERSION4 = "2.0";
|
|
993
|
+
var parseSleepLevels = (raw, logger) => {
|
|
994
|
+
const parsed = [];
|
|
995
|
+
for (const entry of raw) {
|
|
996
|
+
const result = fitSleepLevelSchema.safeParse(entry);
|
|
997
|
+
if (result.success) {
|
|
998
|
+
parsed.push(result.data);
|
|
999
|
+
} else {
|
|
1000
|
+
logger.warn("Skipping malformed sleep_level message", {
|
|
1001
|
+
issues: result.error.issues
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return parsed;
|
|
1006
|
+
};
|
|
1007
|
+
var convertFitToKrdHealthSleep = (messages, logger) => {
|
|
1008
|
+
const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
|
|
1009
|
+
const rawLevels = messages[fitMessageKeySchema.enum.sleepLevelMesgs] ?? [];
|
|
1010
|
+
const fitLevels = parseSleepLevels(rawLevels, logger);
|
|
1011
|
+
const sleep = mapFitSleepLevelsToKrdSleep(fitLevels);
|
|
1012
|
+
return {
|
|
1013
|
+
version: KRD_VERSION4,
|
|
1014
|
+
type: fileTypeSchema.enum.sleep_record,
|
|
1015
|
+
metadata: buildHealthMetadata(fileId),
|
|
1016
|
+
extensions: sleep ? { health: { sleep } } : void 0
|
|
1017
|
+
};
|
|
1018
|
+
};
|
|
1019
|
+
var fitStressLevelSchema = z.object({
|
|
1020
|
+
stressLevelTime: z.union([z.date(), z.string(), z.number()]),
|
|
1021
|
+
stressLevelValue: z.number().int()
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// src/adapters/health/stress/health-stress.converter.ts
|
|
1025
|
+
var HEALTH_VERSION5 = "2.0";
|
|
1026
|
+
var MIN_VALID = 0;
|
|
1027
|
+
var MAX_VALID = 100;
|
|
1028
|
+
var MS_PER_S2 = 1e3;
|
|
1029
|
+
var toEpochMs = (value) => {
|
|
1030
|
+
if (value instanceof Date) return value.getTime();
|
|
1031
|
+
if (typeof value === "number") return value * MS_PER_S2;
|
|
1032
|
+
return new Date(value).getTime();
|
|
1033
|
+
};
|
|
1034
|
+
var isValid = (sample) => sample.stressLevelValue >= MIN_VALID && sample.stressLevelValue <= MAX_VALID;
|
|
1035
|
+
var mean = (values) => Math.round(values.reduce((s, v) => s + v, 0) / values.length);
|
|
1036
|
+
var mapFitStressToKrd = (samples) => {
|
|
1037
|
+
const valid = samples.filter(isValid);
|
|
1038
|
+
if (valid.length === 0) return void 0;
|
|
1039
|
+
const epochs = valid.map((s) => toEpochMs(s.stressLevelTime));
|
|
1040
|
+
const values = valid.map((s) => s.stressLevelValue);
|
|
1041
|
+
return {
|
|
1042
|
+
kind: "stress",
|
|
1043
|
+
version: HEALTH_VERSION5,
|
|
1044
|
+
startTime: new Date(Math.min(...epochs)).toISOString(),
|
|
1045
|
+
endTime: new Date(Math.max(...epochs)).toISOString(),
|
|
1046
|
+
averageLevel: mean(values),
|
|
1047
|
+
peakLevel: Math.max(...values)
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// src/adapters/health/stress/fit-to-krd-health-stress.converter.ts
|
|
1052
|
+
var KRD_VERSION5 = "2.0";
|
|
1053
|
+
var parseSamples = (raw, logger) => {
|
|
1054
|
+
const out = [];
|
|
1055
|
+
for (const entry of raw) {
|
|
1056
|
+
const result = fitStressLevelSchema.safeParse(entry);
|
|
1057
|
+
if (result.success) {
|
|
1058
|
+
out.push(result.data);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
logger.warn("Skipping malformed stress_level message", {
|
|
1062
|
+
issues: result.error.issues
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return out;
|
|
1066
|
+
};
|
|
1067
|
+
var convertFitToKrdHealthStress = (messages, logger) => {
|
|
1068
|
+
const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
|
|
1069
|
+
const samples = parseSamples(
|
|
1070
|
+
messages[fitMessageKeySchema.enum.stressLevelMesgs] ?? [],
|
|
1071
|
+
logger
|
|
1072
|
+
);
|
|
1073
|
+
const stress = mapFitStressToKrd(samples);
|
|
1074
|
+
return {
|
|
1075
|
+
version: KRD_VERSION5,
|
|
1076
|
+
type: fileTypeSchema.enum.stress_episode,
|
|
1077
|
+
metadata: buildHealthMetadata(fileId),
|
|
1078
|
+
extensions: stress ? { health: { stress } } : void 0
|
|
1079
|
+
};
|
|
1080
|
+
};
|
|
1081
|
+
var fitWeightScaleSchema = z.object({
|
|
1082
|
+
timestamp: z.union([z.date(), z.string(), z.number()]),
|
|
1083
|
+
weight: z.number(),
|
|
1084
|
+
percentFat: z.number().optional(),
|
|
1085
|
+
percentHydration: z.number().optional(),
|
|
1086
|
+
visceralFatMass: z.number().optional(),
|
|
1087
|
+
boneMass: z.number().optional(),
|
|
1088
|
+
muscleMass: z.number().optional(),
|
|
1089
|
+
bmi: z.number().optional(),
|
|
1090
|
+
userProfileIndex: z.number().int().optional()
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
// src/adapters/health/weight/health-weight.converter.ts
|
|
1094
|
+
var HEALTH_VERSION6 = "2.0";
|
|
1095
|
+
var FIT_WEIGHT_SCALE = 100;
|
|
1096
|
+
var toIsoString4 = (value) => {
|
|
1097
|
+
if (value instanceof Date) return value.toISOString();
|
|
1098
|
+
if (typeof value === "number") return new Date(value * 1e3).toISOString();
|
|
1099
|
+
return new Date(value).toISOString();
|
|
1100
|
+
};
|
|
1101
|
+
var mapFitWeightScaleToKrd = (fit) => {
|
|
1102
|
+
if (fit.weight <= 0) return void 0;
|
|
1103
|
+
return {
|
|
1104
|
+
kind: "weight",
|
|
1105
|
+
version: HEALTH_VERSION6,
|
|
1106
|
+
measuredAt: toIsoString4(fit.timestamp),
|
|
1107
|
+
weightKilograms: fit.weight / FIT_WEIGHT_SCALE
|
|
1108
|
+
};
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
// src/adapters/health/weight/fit-to-krd-health-weight.converter.ts
|
|
1112
|
+
var KRD_VERSION6 = "2.0";
|
|
1113
|
+
var parseFirstWeight = (raw, logger) => {
|
|
1114
|
+
for (const entry of raw) {
|
|
1115
|
+
const result = fitWeightScaleSchema.safeParse(entry);
|
|
1116
|
+
if (result.success) return result.data;
|
|
1117
|
+
logger.warn("Skipping malformed weight_scale message", {
|
|
1118
|
+
issues: result.error.issues
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
return void 0;
|
|
1122
|
+
};
|
|
1123
|
+
var convertFitToKrdHealthWeight = (messages, logger) => {
|
|
1124
|
+
const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
|
|
1125
|
+
const rawWeights = messages[fitMessageKeySchema.enum.weightScaleMesgs] ?? [];
|
|
1126
|
+
if (rawWeights.length > 1) {
|
|
1127
|
+
logger.warn("Multiple weight_scale messages \u2014 keeping the first only", {
|
|
1128
|
+
count: rawWeights.length
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
const fitWeight = parseFirstWeight(rawWeights, logger);
|
|
1132
|
+
const weight = fitWeight ? mapFitWeightScaleToKrd(fitWeight) : void 0;
|
|
1133
|
+
return {
|
|
1134
|
+
version: KRD_VERSION6,
|
|
1135
|
+
type: fileTypeSchema.enum.weight_measurement,
|
|
1136
|
+
metadata: buildHealthMetadata(fileId),
|
|
1137
|
+
extensions: weight ? { health: { weight } } : void 0
|
|
1138
|
+
};
|
|
1139
|
+
};
|
|
1140
|
+
|
|
677
1141
|
// src/adapters/event/event-type-maps.ts
|
|
678
1142
|
var FIT_EVENT_TO_KRD_TYPE = {
|
|
679
1143
|
timer: "event_timer",
|
|
@@ -1152,7 +1616,7 @@ var convertFitToKrdSession = (data) => {
|
|
|
1152
1616
|
};
|
|
1153
1617
|
|
|
1154
1618
|
// src/adapters/messages/activity.mapper.ts
|
|
1155
|
-
var
|
|
1619
|
+
var KRD_VERSION7 = "1.0";
|
|
1156
1620
|
var convertTimeCreated = (timeCreated) => {
|
|
1157
1621
|
if (timeCreated instanceof Date) return timeCreated.toISOString();
|
|
1158
1622
|
if (typeof timeCreated === "number")
|
|
@@ -1184,7 +1648,7 @@ var mapActivityFileToKRD = (messages, logger) => {
|
|
|
1184
1648
|
const laps = convertFitToKrdLaps(lapMsgs);
|
|
1185
1649
|
const fitExtensions = extractFitExtensions(messages, logger);
|
|
1186
1650
|
return {
|
|
1187
|
-
version:
|
|
1651
|
+
version: KRD_VERSION7,
|
|
1188
1652
|
type: fileTypeSchema.enum.recorded_activity,
|
|
1189
1653
|
metadata: buildKrdMetadata(
|
|
1190
1654
|
fileId,
|
|
@@ -1197,6 +1661,30 @@ var mapActivityFileToKRD = (messages, logger) => {
|
|
|
1197
1661
|
extensions: { fit: fitExtensions }
|
|
1198
1662
|
};
|
|
1199
1663
|
};
|
|
1664
|
+
var HEALTH_DETECTORS = [
|
|
1665
|
+
["sleepLevelMesgs", fileTypeSchema.enum.sleep_record],
|
|
1666
|
+
["bodyCompositionMesgs", fileTypeSchema.enum.body_composition],
|
|
1667
|
+
["weightScaleMesgs", fileTypeSchema.enum.weight_measurement],
|
|
1668
|
+
["hrvStatusSummaryMesgs", fileTypeSchema.enum.hrv_summary],
|
|
1669
|
+
["hrvValueMesgs", fileTypeSchema.enum.hrv_summary],
|
|
1670
|
+
["monitoringMesgs", fileTypeSchema.enum.daily_wellness],
|
|
1671
|
+
["stressLevelMesgs", fileTypeSchema.enum.stress_episode]
|
|
1672
|
+
];
|
|
1673
|
+
var detectFileType = (messages) => {
|
|
1674
|
+
for (const [key, type] of HEALTH_DETECTORS) {
|
|
1675
|
+
const mesgs = messages[key];
|
|
1676
|
+
if (mesgs && mesgs.length > 0) return type;
|
|
1677
|
+
}
|
|
1678
|
+
const workoutMsgs = messages[fitMessageKeySchema.enum.workoutMesgs];
|
|
1679
|
+
if (workoutMsgs && workoutMsgs.length > 0)
|
|
1680
|
+
return fileTypeSchema.enum.structured_workout;
|
|
1681
|
+
const sessionMsgs = messages[fitMessageKeySchema.enum.sessionMesgs];
|
|
1682
|
+
const recordMsgs = messages[fitMessageKeySchema.enum.recordMesgs];
|
|
1683
|
+
if (sessionMsgs && sessionMsgs.length > 0 || recordMsgs && recordMsgs.length > 0) {
|
|
1684
|
+
return fileTypeSchema.enum.recorded_activity;
|
|
1685
|
+
}
|
|
1686
|
+
return fileTypeSchema.enum.structured_workout;
|
|
1687
|
+
};
|
|
1200
1688
|
var DEFAULT_SPORT = sportSchema.enum.cycling;
|
|
1201
1689
|
var mapSportType = (fitSport) => {
|
|
1202
1690
|
if (!fitSport) {
|
|
@@ -1873,7 +2361,7 @@ var validateMessages = (...[fileId, workoutMsg, messages, logger, options = {}])
|
|
|
1873
2361
|
};
|
|
1874
2362
|
|
|
1875
2363
|
// src/adapters/messages/workout.mapper.ts
|
|
1876
|
-
var
|
|
2364
|
+
var KRD_VERSION8 = "1.0";
|
|
1877
2365
|
var mapWorkoutFileToKRD = (messages, logger) => {
|
|
1878
2366
|
const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
|
|
1879
2367
|
const workoutMsg = messages[fitMessageKeySchema.enum.workoutMesgs]?.[0];
|
|
@@ -1883,7 +2371,7 @@ var mapWorkoutFileToKRD = (messages, logger) => {
|
|
|
1883
2371
|
const workout = mapWorkout(workoutMsg, workoutSteps, logger);
|
|
1884
2372
|
const fitExtensions = extractFitExtensions(messages, logger);
|
|
1885
2373
|
return {
|
|
1886
|
-
version:
|
|
2374
|
+
version: KRD_VERSION8,
|
|
1887
2375
|
type: fileTypeSchema.enum.structured_workout,
|
|
1888
2376
|
metadata,
|
|
1889
2377
|
extensions: { structured_workout: workout, fit: fitExtensions }
|
|
@@ -1891,17 +2379,6 @@ var mapWorkoutFileToKRD = (messages, logger) => {
|
|
|
1891
2379
|
};
|
|
1892
2380
|
|
|
1893
2381
|
// src/adapters/messages/messages.mapper.ts
|
|
1894
|
-
var detectFileType = (messages) => {
|
|
1895
|
-
const workoutMsgs = messages[fitMessageKeySchema.enum.workoutMesgs];
|
|
1896
|
-
if (workoutMsgs && workoutMsgs.length > 0)
|
|
1897
|
-
return fileTypeSchema.enum.structured_workout;
|
|
1898
|
-
const sessionMsgs = messages[fitMessageKeySchema.enum.sessionMesgs];
|
|
1899
|
-
const recordMsgs = messages[fitMessageKeySchema.enum.recordMesgs];
|
|
1900
|
-
if (sessionMsgs && sessionMsgs.length > 0 || recordMsgs && recordMsgs.length > 0) {
|
|
1901
|
-
return fileTypeSchema.enum.recorded_activity;
|
|
1902
|
-
}
|
|
1903
|
-
return fileTypeSchema.enum.structured_workout;
|
|
1904
|
-
};
|
|
1905
2382
|
var mapMessagesToKRD = (messages, logger) => {
|
|
1906
2383
|
logger.debug("Mapping FIT messages to KRD", {
|
|
1907
2384
|
messageCount: Object.keys(messages).length
|
|
@@ -1909,6 +2386,18 @@ var mapMessagesToKRD = (messages, logger) => {
|
|
|
1909
2386
|
const fileType = detectFileType(messages);
|
|
1910
2387
|
logger.debug("Detected file type", { fileType });
|
|
1911
2388
|
switch (fileType) {
|
|
2389
|
+
case fileTypeSchema.enum.sleep_record:
|
|
2390
|
+
return convertFitToKrdHealthSleep(messages, logger);
|
|
2391
|
+
case fileTypeSchema.enum.weight_measurement:
|
|
2392
|
+
return convertFitToKrdHealthWeight(messages, logger);
|
|
2393
|
+
case fileTypeSchema.enum.body_composition:
|
|
2394
|
+
return convertFitToKrdHealthBodyComposition(messages, logger);
|
|
2395
|
+
case fileTypeSchema.enum.hrv_summary:
|
|
2396
|
+
return convertFitToKrdHealthHrv(messages, logger);
|
|
2397
|
+
case fileTypeSchema.enum.daily_wellness:
|
|
2398
|
+
return convertFitToKrdHealthDaily(messages, logger);
|
|
2399
|
+
case fileTypeSchema.enum.stress_episode:
|
|
2400
|
+
return convertFitToKrdHealthStress(messages, logger);
|
|
1912
2401
|
case fileTypeSchema.enum.recorded_activity:
|
|
1913
2402
|
return mapActivityFileToKRD(messages, logger);
|
|
1914
2403
|
case fileTypeSchema.enum.structured_workout:
|