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..362bf6acd --- /dev/null +++ b/src/helpers/daily-data-providers/apple-health-heart-rate.ts @@ -0,0 +1,113 @@ +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'] + }; + + 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 })); +} + +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 524a65251..000000000 --- a/src/helpers/daily-data-providers/apple-health-max-heart-rate.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { add, formatISO, parseISO } from "date-fns"; -import queryAllDeviceData from "./query-all-device-data"; - -export default function (startDate: Date, endDate: Date) { - return queryAllDeviceData({ - namespace: "AppleHealth", - type: "HourlyMaximumHeartRate", - observedAfter: add(startDate, { days: -1 }).toISOString(), - observedBefore: add(endDate, { days: 1 }).toISOString() - }).then(function (ddp) { - 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; - } - }); - return data; - }); -} 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" 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..0941113d0 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 } from "@careevolution/mydatahelps-js"; +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) { @@ -12,4 +12,71 @@ export function simpleAvailabilityCheck(namespace: DeviceDataNamespace, type: st return false; }); } -} \ No newline at end of file +} + +// todo: test +export function simpleAvailabilityCheckV2(namespace: DeviceDataV2Namespace, type: string): (modifiedAfter?: Date) => Promise { + 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[]): (modifiedAfter?: Date) => Promise { + 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[] }[], + v2AggregateParameters?: { namespace: DeviceDataV2Namespace, type: string, aggregateFunctions: string | string[] }[], + v2Parameters?: { namespace: DeviceDataV2Namespace, type: string }[]): (modifiedAfter?: Date) => Promise { + return function(modifiedAfter?: Date) { + 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); + }); + } +}; + + \ 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..5454cf015 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: ,