From b203ef49e2b61570b2502db3339c6c5d6682ba42 Mon Sep 17 00:00:00 2001 From: Brandon Charnesky Date: Mon, 30 Sep 2024 12:17:48 -0400 Subject: [PATCH 1/7] Create combinedAvailabilityCheck --- src/helpers/daily-data-types.tsx | 8 ++ .../daily-data-types/availability-check.ts | 5 + src/helpers/daily-data-types/combined.tsx | 104 ++++++++++-------- 3 files changed, 71 insertions(+), 46 deletions(-) diff --git a/src/helpers/daily-data-types.tsx b/src/helpers/daily-data-types.tsx index 9802a4406..ea9eee3e1 100644 --- a/src/helpers/daily-data-types.tsx +++ b/src/helpers/daily-data-types.tsx @@ -7,6 +7,8 @@ export enum DailyDataType { AppleHealthHeartRateRange = "AppleHealthHeartRateRange", AppleHealthHrv = "AppleHealthHrv", AppleHealthMaxHeartRate = "AppleHealthMaxHeartRate", + AppleHealthMinHeartRate = "AppleHealthMinHeartRate", + AppleHealthAverageHeartRate = "AppleHealthAverageHeartRate", AppleHealthRestingHeartRate = "AppleHealthRestingHeartRate", AppleHealthSleepMinutes = "AppleHealthSleepMinutes", AppleHealthRemSleepMinutes = "AppleHealthSleepRemMinutes", @@ -32,6 +34,9 @@ export enum DailyDataType { FitbitFloors = "FitbitFloors", FitbitHrv = "FitbitHrv", FitbitRestingHeartRate = "FitbitRestingHeartRate", + FitbitMaxHeartRate = "FitbitMaxHeartRate", + FitbitMinHeartRate = "FitbitMinHeartRate", + FitbitAverageHearthRate = "FitbitAverageHearthRate", FitbitSleepMinutes = "FitbitSleepMinutes", FitbitLightSleepMinutes = "FitbitLightSleepMinutes", FitbitRemSleepMinutes = "FitbitRemSleepMinutes", @@ -65,6 +70,9 @@ export enum DailyDataType { GoogleFitSteps = "GoogleFitSteps", Steps = "Steps", RestingHeartRate = "RestingHeartRate", + MinHeartRate = "MinHeartRate", + MaxHeartRate = "MaxHeartRate", + AverageHeartRate = "AverageHeartRate", SleepMinutes = "SleepMinutes", HomeAirQuality = "HomeAirQuality", WorkAirQuality = "WorkAirQuality" diff --git a/src/helpers/daily-data-types/availability-check.ts b/src/helpers/daily-data-types/availability-check.ts index 00969bae5..cfdb85434 100644 --- a/src/helpers/daily-data-types/availability-check.ts +++ b/src/helpers/daily-data-types/availability-check.ts @@ -12,4 +12,9 @@ export function simpleAvailabilityCheck(namespace: DeviceDataNamespace, type: st return false; }); } +} + +export function combinedAvailabilityCheck(parameters: { namespace: DeviceDataNamespace, type: string | string[] }[], modifiedAfter?: Date) { + var checks = parameters.map(param => simpleAvailabilityCheck(param.namespace, param.type)); + return Promise.allSettled(checks.map(check => check(modifiedAfter))).then(results => results.some(result => result)); } \ No newline at end of file diff --git a/src/helpers/daily-data-types/combined.tsx b/src/helpers/daily-data-types/combined.tsx index 9eb01334a..f6e212e40 100644 --- a/src/helpers/daily-data-types/combined.tsx +++ b/src/helpers/daily-data-types/combined.tsx @@ -6,7 +6,7 @@ import React from "react"; import { defaultFormatter, heartRateFormatter, minutesFormatter, sleepYAxisConverter } from "./formatters"; import combinedRestingHeartRate from "../daily-data-providers/combined-resting-heart-rate"; import { combinedSleepDataProvider, combinedStepsDataProvider } from "../daily-data-providers"; -import { simpleAvailabilityCheck } from "./availability-check"; +import { simpleAvailabilityCheck, combinedAvailabilityCheck } from "./availability-check"; let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ { @@ -14,47 +14,70 @@ let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ type: DailyDataType.RestingHeartRate, dataProvider: combinedRestingHeartRate, availabilityCheck: function (modifiedAfter?: Date) { - return simpleAvailabilityCheck("AppleHealth", ["RestingHeartRate"])(modifiedAfter).then(function (result) { - if (!result) { - return simpleAvailabilityCheck("Fitbit", ["RestingHeartRate"])(modifiedAfter).then(function (result) { - if (!result) { - return simpleAvailabilityCheck("Garmin", ["RestingHeartRateInBeatsPerMinute"])(modifiedAfter); - } - else { - return result; - } - }) - } - else { - return result; - } - }) + return combinedAvailabilityCheck( [ + { namespace: "AppleHealth", type: ["RestingHeartRate"] }, + { namespace: "Fitbit", type: ["RestingHeartRate"] }, + { namespace: "Garmin", type: ["RestingHeartRateInBeatsPerMinute"] } ], modifiedAfter); }, labelKey: "resting-heart-rate", icon: , formatter: heartRateFormatter, previewDataRange: [40, 100] }, + { + dataSource: "Unified", + type: DailyDataType.MaxHeartRate, + dataProvider: combinedRestingHeartRate, + availabilityCheck: function (modifiedAfter?: Date) { + return combinedAvailabilityCheck( [ + { namespace: "AppleHealth", type: ["AppleHealthMaxHeartRate"] }, + { namespace: "Fitbit", type: ["FitbitMaxHeartRate"] }, + { namespace: "Garmin", type: ["GarminMaxHeartRate"] } ], modifiedAfter); + }, + labelKey: "max-heart-rate", + icon: , + formatter: heartRateFormatter, + previewDataRange: [100, 200] + }, + { + dataSource: "Unified", + type: DailyDataType.MinHeartRate, + dataProvider: combinedRestingHeartRate, + availabilityCheck: function (modifiedAfter?: Date) { + return combinedAvailabilityCheck( [ + { namespace: "AppleHealth", type: ["AppleHealthMinHeartRate"] }, + { namespace: "Fitbit", type: ["FitbitMinHeartRate"] }, + { namespace: "Garmin", type: ["GarminMinHeartRate"] } ], modifiedAfter); + }, + labelKey: "min-heart-rate", + icon: , + formatter: heartRateFormatter, + previewDataRange: [40, 100] + }, + { + dataSource: "Unified", + type: DailyDataType.AverageHeartRate, + dataProvider: combinedRestingHeartRate, + availabilityCheck: function (modifiedAfter?: Date) { + return combinedAvailabilityCheck( [ + { namespace: "AppleHealth", type: ["AppleHealthAverageHeartRate"] }, + { namespace: "Fitbit", type: ["FitbitAverageHeartRate"] }, + { namespace: "Garmin", type: ["GarminAverageHeartRate"] } ], modifiedAfter); + }, + labelKey: "average-heart-rate", + icon: , + formatter: heartRateFormatter, + previewDataRange: [50, 110] + }, { dataSource: "Unified", type: DailyDataType.Steps, dataProvider: combinedStepsDataProvider, availabilityCheck: function (modifiedAfter?: Date) { - return simpleAvailabilityCheck("AppleHealth", ["Steps"])(modifiedAfter).then(function (result) { - if (!result) { - return simpleAvailabilityCheck("Fitbit", ["Steps"])(modifiedAfter).then(function (result) { - if (!result) { - return simpleAvailabilityCheck("Garmin", ["Steps"])(modifiedAfter); - } - else { - return result; - } - }) - } - else { - return result; - } - }); + return combinedAvailabilityCheck( [ + { namespace: "AppleHealth", type: ["Steps"] }, + { namespace: "Fitbit", type: ["Steps"] }, + { namespace: "Garmin", type: ["Steps"] } ], modifiedAfter); }, labelKey: "steps", icon: , @@ -66,21 +89,10 @@ let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ type: DailyDataType.SleepMinutes, dataProvider: combinedSleepDataProvider, availabilityCheck: function (modifiedAfter?: Date) { - return simpleAvailabilityCheck("AppleHealth", ["SleepAnalysisInterval"])(modifiedAfter).then(function (result) { - if (!result) { - return simpleAvailabilityCheck("Fitbit", ["SleepLevelRem", "SleepLevelLight", "SleepLevelDeep", "SleepLevelAsleep"])(modifiedAfter).then(function (result) { - if (!result) { - return simpleAvailabilityCheck("Garmin", ["Sleep"])(modifiedAfter); - } - else { - return result; - } - }) - } - else { - return result; - } - }) + return combinedAvailabilityCheck( [ + { namespace: "AppleHealth", type: ["SleepAnalysisInterval"] }, + { namespace: "Fitbit", type: ["SleepLevelRem", "SleepLevelLight", "SleepLevelDeep", "SleepLevelAsleep"] }, + { namespace: "Garmin", type: ["Sleep"] } ], modifiedAfter); }, labelKey: "sleep-time", icon: , From d6808b7f89b8572c1e9c57427c2475ca36e4aa7d Mon Sep 17 00:00:00 2001 From: Brandon Charnesky Date: Mon, 30 Sep 2024 15:12:45 -0400 Subject: [PATCH 2/7] Add availability checks for v2 queries --- .../daily-data-types/availability-check.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/helpers/daily-data-types/availability-check.ts b/src/helpers/daily-data-types/availability-check.ts index cfdb85434..c20359698 100644 --- a/src/helpers/daily-data-types/availability-check.ts +++ b/src/helpers/daily-data-types/availability-check.ts @@ -1,4 +1,4 @@ -import MyDataHelps, { DeviceDataNamespace, DeviceDataPointQuery } from "@careevolution/mydatahelps-js"; +import MyDataHelps, { DeviceDataNamespace, DeviceDataPointQuery, DeviceDataV2AggregateQuery, DeviceDataV2Namespace, DeviceDataV2Query } from "@careevolution/mydatahelps-js"; export function simpleAvailabilityCheck(namespace: DeviceDataNamespace, type: string | string[]) { return function (modifiedAfter?: Date) { @@ -14,6 +14,49 @@ export function simpleAvailabilityCheck(namespace: DeviceDataNamespace, type: st } } +// todo: test +export function simpleAvailabilityCheckV2(namespace: DeviceDataV2Namespace, type: string) { + return function (modifiedAfter?: Date) { + var parameters: DeviceDataV2Query = { + namespace: namespace, + type: type, + limit: 1 + }; + if (modifiedAfter) { + parameters.modifiedAfter = modifiedAfter.toISOString(); + } + + return MyDataHelps.queryDeviceDataV2(parameters).then(function (result) { + return result.deviceDataPoints.length > 0; + }).catch(function () { + return false; + }); + } +} + +// todo: test +export function simpleAvailabilityCheckV2Aggregate(namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[]) { + return function (modifiedAfter?: Date) { + var parameters: DeviceDataV2AggregateQuery = { + namespace: namespace, + type: type, + limit: 1, + intervalAmount: 1, + intervalType: "Days", + aggregateFunctions: aggregateFunctions + }; + if (modifiedAfter) { + parameters.modifiedAfter = modifiedAfter.toISOString(); + } + + return MyDataHelps.queryDeviceDataV2Aggregate(parameters).then(function (result) { + return result.intervals.length > 0; + }).catch(function () { + return false; + }); + } +} + export function combinedAvailabilityCheck(parameters: { namespace: DeviceDataNamespace, type: string | string[] }[], modifiedAfter?: Date) { var checks = parameters.map(param => simpleAvailabilityCheck(param.namespace, param.type)); return Promise.allSettled(checks.map(check => check(modifiedAfter))).then(results => results.some(result => result)); From fa66e69d3c360d4e2414774123d9505ece2ec1a6 Mon Sep 17 00:00:00 2001 From: Brandon Charnesky Date: Mon, 30 Sep 2024 15:13:57 -0400 Subject: [PATCH 3/7] Add v2Aggregate query to applehealth-max-heart-rate --- .../apple-health-max-heart-rate.ts | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/helpers/daily-data-providers/apple-health-max-heart-rate.ts b/src/helpers/daily-data-providers/apple-health-max-heart-rate.ts index 524a65251..efe7803bb 100644 --- a/src/helpers/daily-data-providers/apple-health-max-heart-rate.ts +++ b/src/helpers/daily-data-providers/apple-health-max-heart-rate.ts @@ -1,24 +1,47 @@ -import { add, formatISO, parseISO } from "date-fns"; +import { add, endOfDay, formatISO, parseISO, startOfDay } from "date-fns"; import queryAllDeviceData from "./query-all-device-data"; +import queryAllDeviceDataV2Aggregates from "../query-all-device-data-v2-aggregates"; +import { DeviceDataPointQuery, DeviceDataV2AggregateQuery } from "@careevolution/mydatahelps-js/types"; -export default function (startDate: Date, endDate: Date) { - return queryAllDeviceData({ +export default function (startDate: Date, endDate: Date): Promise<{ [key: string]: number }> { + const v2params: DeviceDataV2AggregateQuery = { + namespace: 'AppleHealth', + type: 'Heart Rate', + observedAfter: startOfDay(startDate).toISOString(), + observedBefore: endOfDay(endDate).toISOString(), + intervalAmount: 1, + intervalType: 'Days', + aggregateFunctions: ['max'] + }; + + const v1params: DeviceDataPointQuery = { namespace: "AppleHealth", type: "HourlyMaximumHeartRate", observedAfter: add(startDate, { days: -1 }).toISOString(), observedBefore: add(endDate, { days: 1 }).toISOString() - }).then(function (ddp) { + } + + return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params), queryAllDeviceData(v1params) ]).then(queryResults => { var data: { [key: string]: number } = {}; - ddp.forEach((d) => { - if (!d.startDate) { return; } - var day = formatISO(parseISO(d.startDate)).substring(0, 10); - var value = parseFloat(d.value); - if (!data[day]) { - data[day] = value; - } else if (value > data[day]) { - data[day] = value; - } - }); + if (queryResults[0].status === 'fulfilled') { + queryResults[0].value.forEach((aggregate) => { + data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['max']; + }); + } + if (queryResults[1].status === 'fulfilled') { + queryResults[1].value.forEach((d) => { + if (!d.startDate) { return; } + var day = formatISO(parseISO(d.startDate)).substring(0, 10); + var value = parseFloat(d.value); + if (!data[day]) { + data[day] = value; + } else if (value > data[day]) { + data[day] = value; + } + }); + } return data; - }); + + }, () => ({} as { [key: string]: number })); } + From e6855daf5a351759aff65cf4a6d4ea9c07471c66 Mon Sep 17 00:00:00 2001 From: Brandon Charnesky Date: Mon, 30 Sep 2024 15:21:58 -0400 Subject: [PATCH 4/7] Update combinedAvailabilityCheck for all queries --- src/helpers/daily-data-types/availability-check.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/helpers/daily-data-types/availability-check.ts b/src/helpers/daily-data-types/availability-check.ts index c20359698..d1f9fe1ab 100644 --- a/src/helpers/daily-data-types/availability-check.ts +++ b/src/helpers/daily-data-types/availability-check.ts @@ -57,7 +57,14 @@ export function simpleAvailabilityCheckV2Aggregate(namespace: DeviceDataV2Namesp } } -export function combinedAvailabilityCheck(parameters: { namespace: DeviceDataNamespace, type: string | string[] }[], modifiedAfter?: Date) { +export function combinedAvailabilityCheck( + parameters: { namespace: DeviceDataNamespace, type: string | string[] }[], + v2AggregateParameters: { namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[] }[], + v2Parameters: { namespace: DeviceDataV2Namespace, type: string }[], + modifiedAfter?: Date) { var checks = parameters.map(param => simpleAvailabilityCheck(param.namespace, param.type)); + checks.concat( v2AggregateParameters.map(param => simpleAvailabilityCheckV2Aggregate(param.namespace, param.type, param.aggregateFunctions)) ); + checks.concat( v2Parameters.map(param => simpleAvailabilityCheckV2(param.namespace, param.type)) ); + return Promise.allSettled(checks.map(check => check(modifiedAfter))).then(results => results.some(result => result)); } \ No newline at end of file From d37fb6c9a39bb57645a9fb91c5f7194c29f37bcb Mon Sep 17 00:00:00 2001 From: Brandon Charnesky Date: Mon, 30 Sep 2024 15:44:17 -0400 Subject: [PATCH 5/7] Update combinedAvailabilityCheck --- .../daily-data-types/availability-check.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/helpers/daily-data-types/availability-check.ts b/src/helpers/daily-data-types/availability-check.ts index d1f9fe1ab..2e1c9c883 100644 --- a/src/helpers/daily-data-types/availability-check.ts +++ b/src/helpers/daily-data-types/availability-check.ts @@ -1,6 +1,6 @@ import MyDataHelps, { DeviceDataNamespace, DeviceDataPointQuery, DeviceDataV2AggregateQuery, DeviceDataV2Namespace, DeviceDataV2Query } from "@careevolution/mydatahelps-js"; -export function simpleAvailabilityCheck(namespace: DeviceDataNamespace, type: string | string[]) { +export function simpleAvailabilityCheck(namespace: DeviceDataNamespace, type: string | string[]): (modifiedAfter?: Date) => Promise { return function (modifiedAfter?: Date) { var parameters: DeviceDataPointQuery = { namespace: namespace, type: type, limit: 1 }; if (modifiedAfter) { @@ -15,7 +15,7 @@ export function simpleAvailabilityCheck(namespace: DeviceDataNamespace, type: st } // todo: test -export function simpleAvailabilityCheckV2(namespace: DeviceDataV2Namespace, type: string) { +export function simpleAvailabilityCheckV2(namespace: DeviceDataV2Namespace, type: string): (modifiedAfter?: Date) => Promise { return function (modifiedAfter?: Date) { var parameters: DeviceDataV2Query = { namespace: namespace, @@ -35,7 +35,7 @@ export function simpleAvailabilityCheckV2(namespace: DeviceDataV2Namespace, type } // todo: test -export function simpleAvailabilityCheckV2Aggregate(namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[]) { +export function simpleAvailabilityCheckV2Aggregate(namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[]): (modifiedAfter?: Date) => Promise { return function (modifiedAfter?: Date) { var parameters: DeviceDataV2AggregateQuery = { namespace: namespace, @@ -58,13 +58,18 @@ export function simpleAvailabilityCheckV2Aggregate(namespace: DeviceDataV2Namesp } export function combinedAvailabilityCheck( - parameters: { namespace: DeviceDataNamespace, type: string | string[] }[], - v2AggregateParameters: { namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[] }[], - v2Parameters: { namespace: DeviceDataV2Namespace, type: string }[], - modifiedAfter?: Date) { - var checks = parameters.map(param => simpleAvailabilityCheck(param.namespace, param.type)); - checks.concat( v2AggregateParameters.map(param => simpleAvailabilityCheckV2Aggregate(param.namespace, param.type, param.aggregateFunctions)) ); - checks.concat( v2Parameters.map(param => simpleAvailabilityCheckV2(param.namespace, param.type)) ); + parameters: { namespace: DeviceDataNamespace, type: string | string[] }[], + v2AggregateParameters: { namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[] }[], + v2Parameters: { namespace: DeviceDataV2Namespace, type: string }[]): (modifiedAfter?: Date) => Promise { + return function(modifiedAfter?: Date) { + var checks = parameters.map(param => simpleAvailabilityCheck(param.namespace, param.type)); + checks.concat( v2AggregateParameters.map(param => simpleAvailabilityCheckV2Aggregate(param.namespace, param.type, param.aggregateFunctions)) ); + checks.concat( v2Parameters.map(param => simpleAvailabilityCheckV2(param.namespace, param.type)) ); - return Promise.allSettled(checks.map(check => check(modifiedAfter))).then(results => results.some(result => result)); -} \ No newline at end of file + return Promise.allSettled(checks.map(check => check(modifiedAfter))).then(function (results) { + return results.some(result => result.status === 'fulfilled' && result.value === true); + }); + } +}; + + \ No newline at end of file From 65ba0e8394928978d5be36217944aa4f9ab76d45 Mon Sep 17 00:00:00 2001 From: Brandon Charnesky Date: Mon, 30 Sep 2024 15:57:08 -0400 Subject: [PATCH 6/7] Create fitbit and applehealth min/max/avg heart rate data providers --- .../apple-health-heart-rate.ts | 95 +++++++++++++++++++ .../apple-health-max-heart-rate.ts | 47 --------- .../daily-data-providers/fitbit-heart-rate.ts | 76 +++++++++++++++ src/helpers/daily-data-providers/index.ts | 4 +- 4 files changed, 174 insertions(+), 48 deletions(-) create mode 100644 src/helpers/daily-data-providers/apple-health-heart-rate.ts delete mode 100644 src/helpers/daily-data-providers/apple-health-max-heart-rate.ts create mode 100644 src/helpers/daily-data-providers/fitbit-heart-rate.ts diff --git a/src/helpers/daily-data-providers/apple-health-heart-rate.ts b/src/helpers/daily-data-providers/apple-health-heart-rate.ts new file mode 100644 index 000000000..9a9917bbb --- /dev/null +++ b/src/helpers/daily-data-providers/apple-health-heart-rate.ts @@ -0,0 +1,95 @@ +import { add, endOfDay, formatISO, parseISO, startOfDay } from "date-fns"; +import queryAllDeviceData from "./query-all-device-data"; +import queryAllDeviceDataV2Aggregates from "../query-all-device-data-v2-aggregates"; +import { DeviceDataPointQuery, DeviceDataV2AggregateQuery } from "@careevolution/mydatahelps-js/types"; + +export function maxHeartRate (startDate: Date, endDate: Date): Promise<{ [key: string]: number }> { + const v2params: DeviceDataV2AggregateQuery = { + namespace: 'AppleHealth', + type: 'Heart Rate', + observedAfter: startOfDay(startDate).toISOString(), + observedBefore: endOfDay(endDate).toISOString(), + intervalAmount: 1, + intervalType: 'Days', + aggregateFunctions: ['max'] + }; + + const v1params: DeviceDataPointQuery = { + namespace: "AppleHealth", + type: "HourlyMaximumHeartRate", + observedAfter: add(startDate, { days: -1 }).toISOString(), + observedBefore: add(endDate, { days: 1 }).toISOString() + } + + return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params), queryAllDeviceData(v1params) ]).then(queryResults => { + var data: { [key: string]: number } = {}; + if (queryResults[0].status === 'fulfilled') { + queryResults[0].value.forEach((aggregate) => { + data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['max']; + }); + } + if (queryResults[1].status === 'fulfilled') { + queryResults[1].value.forEach((d) => { + if (!d.startDate) { return; } + var day = formatISO(parseISO(d.startDate)).substring(0, 10); + var value = parseFloat(d.value); + if (!data[day]) { + data[day] = value; + } else if (value > data[day]) { + data[day] = value; + } + }); + } + return data; + + }, () => ({} as { [key: string]: number })); +} + +export function minHeartRate (startDate: Date, endDate: Date): Promise<{ [key: string]: number }> { + const v2params: DeviceDataV2AggregateQuery = { + namespace: 'AppleHealth', + type: 'Heart Rate', + observedAfter: startOfDay(startDate).toISOString(), + observedBefore: endOfDay(endDate).toISOString(), + intervalAmount: 1, + intervalType: 'Days', + aggregateFunctions: ['min'] + }; + + return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params) ]).then(queryResults => { + var data: { [key: string]: number } = {}; + if (queryResults[0].status === 'fulfilled') { + queryResults[0].value.forEach((aggregate) => { + data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['min']; + }); + } + + return data; + + }, () => ({} as { [key: string]: number })); +} + +export function averageHeartRate (startDate: Date, endDate: Date): Promise<{ [key: string]: number }> { + const v2params: DeviceDataV2AggregateQuery = { + namespace: 'AppleHealth', + type: 'Heart Rate', + observedAfter: startOfDay(startDate).toISOString(), + observedBefore: endOfDay(endDate).toISOString(), + intervalAmount: 1, + intervalType: 'Days', + aggregateFunctions: ['average'] + }; + + return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params) ]).then(queryResults => { + var data: { [key: string]: number } = {}; + if (queryResults[0].status === 'fulfilled') { + queryResults[0].value.forEach((aggregate) => { + data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['average']; + }); + } + + return data; + + }, () => ({} as { [key: string]: number })); +} + diff --git a/src/helpers/daily-data-providers/apple-health-max-heart-rate.ts b/src/helpers/daily-data-providers/apple-health-max-heart-rate.ts deleted file mode 100644 index efe7803bb..000000000 --- a/src/helpers/daily-data-providers/apple-health-max-heart-rate.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { add, endOfDay, formatISO, parseISO, startOfDay } from "date-fns"; -import queryAllDeviceData from "./query-all-device-data"; -import queryAllDeviceDataV2Aggregates from "../query-all-device-data-v2-aggregates"; -import { DeviceDataPointQuery, DeviceDataV2AggregateQuery } from "@careevolution/mydatahelps-js/types"; - -export default function (startDate: Date, endDate: Date): Promise<{ [key: string]: number }> { - const v2params: DeviceDataV2AggregateQuery = { - namespace: 'AppleHealth', - type: 'Heart Rate', - observedAfter: startOfDay(startDate).toISOString(), - observedBefore: endOfDay(endDate).toISOString(), - intervalAmount: 1, - intervalType: 'Days', - aggregateFunctions: ['max'] - }; - - const v1params: DeviceDataPointQuery = { - namespace: "AppleHealth", - type: "HourlyMaximumHeartRate", - observedAfter: add(startDate, { days: -1 }).toISOString(), - observedBefore: add(endDate, { days: 1 }).toISOString() - } - - return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params), queryAllDeviceData(v1params) ]).then(queryResults => { - var data: { [key: string]: number } = {}; - if (queryResults[0].status === 'fulfilled') { - queryResults[0].value.forEach((aggregate) => { - data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['max']; - }); - } - if (queryResults[1].status === 'fulfilled') { - queryResults[1].value.forEach((d) => { - if (!d.startDate) { return; } - var day = formatISO(parseISO(d.startDate)).substring(0, 10); - var value = parseFloat(d.value); - if (!data[day]) { - data[day] = value; - } else if (value > data[day]) { - data[day] = value; - } - }); - } - return data; - - }, () => ({} as { [key: string]: number })); -} - diff --git a/src/helpers/daily-data-providers/fitbit-heart-rate.ts b/src/helpers/daily-data-providers/fitbit-heart-rate.ts new file mode 100644 index 000000000..1988a8d17 --- /dev/null +++ b/src/helpers/daily-data-providers/fitbit-heart-rate.ts @@ -0,0 +1,76 @@ +import { add, endOfDay, formatISO, parseISO, startOfDay } from "date-fns"; +import queryAllDeviceDataV2Aggregates from "../query-all-device-data-v2-aggregates"; +import { DeviceDataV2AggregateQuery } from "@careevolution/mydatahelps-js/types"; + +export function fitbitMaxHeartRate (startDate: Date, endDate: Date): Promise<{ [key: string]: number }> { + const v2params: DeviceDataV2AggregateQuery = { + namespace: 'Fitbit', + type: 'activities-heart-intraday', + observedAfter: startOfDay(startDate).toISOString(), + observedBefore: endOfDay(endDate).toISOString(), + intervalAmount: 1, + intervalType: 'Days', + aggregateFunctions: ['max'] + }; + + return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params) ]).then(queryResults => { + var data: { [key: string]: number } = {}; + if (queryResults[0].status === 'fulfilled') { + queryResults[0].value.forEach((aggregate) => { + data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['max']; + }); + } + + return data; + + }, () => ({} as { [key: string]: number })); +} + +export function fitbitMinHeartRate (startDate: Date, endDate: Date): Promise<{ [key: string]: number }> { + const v2params: DeviceDataV2AggregateQuery = { + namespace: 'Fitbit', + type: 'activities-heart-intraday', + observedAfter: startOfDay(startDate).toISOString(), + observedBefore: endOfDay(endDate).toISOString(), + intervalAmount: 1, + intervalType: 'Days', + aggregateFunctions: ['min'] + }; + + return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params) ]).then(queryResults => { + var data: { [key: string]: number } = {}; + if (queryResults[0].status === 'fulfilled') { + queryResults[0].value.forEach((aggregate) => { + data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['min']; + }); + } + + return data; + + }, () => ({} as { [key: string]: number })); +} + +export function fitbitAverageHeartRate (startDate: Date, endDate: Date): Promise<{ [key: string]: number }> { + const v2params: DeviceDataV2AggregateQuery = { + namespace: 'Fitbit', + type: 'activities-heart-intraday', + observedAfter: startOfDay(startDate).toISOString(), + observedBefore: endOfDay(endDate).toISOString(), + intervalAmount: 1, + intervalType: 'Days', + aggregateFunctions: ['average'] + }; + + return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params) ]).then(queryResults => { + var data: { [key: string]: number } = {}; + if (queryResults[0].status === 'fulfilled') { + queryResults[0].value.forEach((aggregate) => { + data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['average']; + }); + } + + return data; + + }, () => ({} as { [key: string]: number })); +} + diff --git a/src/helpers/daily-data-providers/index.ts b/src/helpers/daily-data-providers/index.ts index 2d8c5b462..556e6322d 100644 --- a/src/helpers/daily-data-providers/index.ts +++ b/src/helpers/daily-data-providers/index.ts @@ -1,7 +1,9 @@ export { default as appleHealthFlightsClimbedDataProvider } from "./apple-health-flights-climbed" export { default as appleHealthHrvDataProvider } from "./apple-health-hrv" export { default as appleHealthHeartRateRangeDataProvider } from "./apple-health-heart-rate-range" -export { default as appleHealthMaxHeartRateDataProvider } from "./apple-health-max-heart-rate" +export { maxHeartRate as appleHealthMaxHeartRateDataProvider } from "./apple-health-heart-rate" +export { minHeartRate as appleHealthMinHeartRateDataProvider } from "./apple-health-heart-rate" +export { averageHeartRate as appleHealthAverageHeartRateDataProvider } from "./apple-health-heart-rate" export { default as appleHealthRestingHeartRateDataProvider } from "./apple-health-resting-heart-rate" export { asleepTime as appleHealthSleepDataProvider } from "./apple-health-sleep" export { asleepRemTime as appleHealthSleepRemDataProvider } from "./apple-health-sleep" From a5ebb5d296d8d6acef7c4d30df4a1b7c6c088e75 Mon Sep 17 00:00:00 2001 From: Brandon Charnesky Date: Mon, 30 Sep 2024 16:11:01 -0400 Subject: [PATCH 7/7] Update combinedAvailabilityCheck --- .../apple-health-heart-rate.ts | 22 +++++++++++++++++-- .../daily-data-types/availability-check.ts | 21 ++++++++++++------ src/helpers/daily-data-types/combined.tsx | 12 +++++----- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/helpers/daily-data-providers/apple-health-heart-rate.ts b/src/helpers/daily-data-providers/apple-health-heart-rate.ts index 9a9917bbb..362bf6acd 100644 --- a/src/helpers/daily-data-providers/apple-health-heart-rate.ts +++ b/src/helpers/daily-data-providers/apple-health-heart-rate.ts @@ -56,14 +56,32 @@ export function minHeartRate (startDate: Date, endDate: Date): Promise<{ [key: s aggregateFunctions: ['min'] }; - return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params) ]).then(queryResults => { + const v1params: DeviceDataPointQuery = { + namespace: "AppleHealth", + type: "HourlyMinimumHeartRate", + observedAfter: add(startDate, { days: -1 }).toISOString(), + observedBefore: add(endDate, { days: 1 }).toISOString() + } + + return Promise.allSettled([ queryAllDeviceDataV2Aggregates(v2params), queryAllDeviceData(v1params) ]).then(queryResults => { var data: { [key: string]: number } = {}; if (queryResults[0].status === 'fulfilled') { queryResults[0].value.forEach((aggregate) => { data[formatISO(parseISO(aggregate.date)).substring(0, 10)] = aggregate.statistics['min']; }); } - + if (queryResults[1].status === 'fulfilled') { + queryResults[1].value.forEach((d) => { + if (!d.startDate) { return; } + var day = formatISO(parseISO(d.startDate)).substring(0, 10); + var value = parseFloat(d.value); + if (!data[day]) { + data[day] = value; + } else if (value < data[day] && value > 0) { + data[day] = value; + } + }); + } return data; }, () => ({} as { [key: string]: number })); diff --git a/src/helpers/daily-data-types/availability-check.ts b/src/helpers/daily-data-types/availability-check.ts index 2e1c9c883..0941113d0 100644 --- a/src/helpers/daily-data-types/availability-check.ts +++ b/src/helpers/daily-data-types/availability-check.ts @@ -58,14 +58,21 @@ export function simpleAvailabilityCheckV2Aggregate(namespace: DeviceDataV2Namesp } export function combinedAvailabilityCheck( - parameters: { namespace: DeviceDataNamespace, type: string | string[] }[], - v2AggregateParameters: { namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[] }[], - v2Parameters: { namespace: DeviceDataV2Namespace, type: string }[]): (modifiedAfter?: Date) => Promise { + parameters?: { namespace: DeviceDataNamespace, type: string | string[] }[], + v2AggregateParameters?: { namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[] }[], + v2Parameters?: { namespace: DeviceDataV2Namespace, type: string }[]): (modifiedAfter?: Date) => Promise { return function(modifiedAfter?: Date) { - var checks = parameters.map(param => simpleAvailabilityCheck(param.namespace, param.type)); - checks.concat( v2AggregateParameters.map(param => simpleAvailabilityCheckV2Aggregate(param.namespace, param.type, param.aggregateFunctions)) ); - checks.concat( v2Parameters.map(param => simpleAvailabilityCheckV2(param.namespace, param.type)) ); - + var checks: any[] = []; + if (parameters) { + checks.concat( parameters.map(param => simpleAvailabilityCheck(param.namespace, param.type)) ); + } + if (v2AggregateParameters) { + checks.concat( v2AggregateParameters.map(param => simpleAvailabilityCheckV2Aggregate(param.namespace, param.type, param.aggregateFunctions)) ); + } + if (v2Parameters) { + checks.concat( v2Parameters.map(param => simpleAvailabilityCheckV2(param.namespace, param.type)) ); + } + return Promise.allSettled(checks.map(check => check(modifiedAfter))).then(function (results) { return results.some(result => result.status === 'fulfilled' && result.value === true); }); diff --git a/src/helpers/daily-data-types/combined.tsx b/src/helpers/daily-data-types/combined.tsx index f6e212e40..5454cf015 100644 --- a/src/helpers/daily-data-types/combined.tsx +++ b/src/helpers/daily-data-types/combined.tsx @@ -17,7 +17,7 @@ let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ return combinedAvailabilityCheck( [ { namespace: "AppleHealth", type: ["RestingHeartRate"] }, { namespace: "Fitbit", type: ["RestingHeartRate"] }, - { namespace: "Garmin", type: ["RestingHeartRateInBeatsPerMinute"] } ], modifiedAfter); + { namespace: "Garmin", type: ["RestingHeartRateInBeatsPerMinute"] } ] )( modifiedAfter ); }, labelKey: "resting-heart-rate", icon: , @@ -32,7 +32,7 @@ let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ return combinedAvailabilityCheck( [ { namespace: "AppleHealth", type: ["AppleHealthMaxHeartRate"] }, { namespace: "Fitbit", type: ["FitbitMaxHeartRate"] }, - { namespace: "Garmin", type: ["GarminMaxHeartRate"] } ], modifiedAfter); + { namespace: "Garmin", type: ["GarminMaxHeartRate"] } ] )( modifiedAfter ); }, labelKey: "max-heart-rate", icon: , @@ -47,7 +47,7 @@ let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ return combinedAvailabilityCheck( [ { namespace: "AppleHealth", type: ["AppleHealthMinHeartRate"] }, { namespace: "Fitbit", type: ["FitbitMinHeartRate"] }, - { namespace: "Garmin", type: ["GarminMinHeartRate"] } ], modifiedAfter); + { namespace: "Garmin", type: ["GarminMinHeartRate"] } ] )( modifiedAfter ); }, labelKey: "min-heart-rate", icon: , @@ -62,7 +62,7 @@ let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ return combinedAvailabilityCheck( [ { namespace: "AppleHealth", type: ["AppleHealthAverageHeartRate"] }, { namespace: "Fitbit", type: ["FitbitAverageHeartRate"] }, - { namespace: "Garmin", type: ["GarminAverageHeartRate"] } ], modifiedAfter); + { namespace: "Garmin", type: ["GarminAverageHeartRate"] } ] )( modifiedAfter ); }, labelKey: "average-heart-rate", icon: , @@ -77,7 +77,7 @@ let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ return combinedAvailabilityCheck( [ { namespace: "AppleHealth", type: ["Steps"] }, { namespace: "Fitbit", type: ["Steps"] }, - { namespace: "Garmin", type: ["Steps"] } ], modifiedAfter); + { namespace: "Garmin", type: ["Steps"] } ] )( modifiedAfter); }, labelKey: "steps", icon: , @@ -92,7 +92,7 @@ let combinedTypeDefinitions: DailyDataTypeDefinition[] = [ return combinedAvailabilityCheck( [ { namespace: "AppleHealth", type: ["SleepAnalysisInterval"] }, { namespace: "Fitbit", type: ["SleepLevelRem", "SleepLevelLight", "SleepLevelDeep", "SleepLevelAsleep"] }, - { namespace: "Garmin", type: ["Sleep"] } ], modifiedAfter); + { namespace: "Garmin", type: ["Sleep"] } ] )( modifiedAfter ); }, labelKey: "sleep-time", icon: ,