diff --git a/__tests__/fixtures/daily-data-providers.ts b/__tests__/fixtures/daily-data-providers.ts index 502841b43..0f94a80f9 100644 --- a/__tests__/fixtures/daily-data-providers.ts +++ b/__tests__/fixtures/daily-data-providers.ts @@ -1,3 +1,5 @@ +import * as dailyDataAggregateFunctions from '../../src/helpers/daily-data-providers/daily-data/daily-data-aggregate'; +import { AggregateFunction } from '../../src/helpers/daily-data-providers/daily-data/daily-data-aggregate'; import * as dailyDataQueryFunctions from '../../src/helpers/daily-data-providers/daily-data/daily-data-query'; import { DailyData, DailyDataDateFunction, DailyDataV2, DailyDataValueFunction, DeviceDataV2QueryFilters } from '../../src/helpers/daily-data-providers/daily-data'; import * as dailyDataResultFunctions from '../../src/helpers/daily-data-providers/daily-data/daily-data-result'; @@ -108,6 +110,43 @@ export function setupDailyDataV2( ); } +export function setupAggregateDailyData( + expectedNamespace: DeviceDataV2Namespace, + expectedType: string, + expectedStartDate: Date, + expectedEndDate: Date, + expectedAggregateFn: AggregateFunction, + result: DailyDataQueryResult, + optionalScaleFactor?: number +): void { + jest.spyOn(dailyDataAggregateFunctions, 'queryAggregateDailyData').mockImplementation( + ( + actualNamespace: DeviceDataV2Namespace, + actualType: string, + actualStartDate: Date, + actualEndDate: Date, + actualAggregateFn: AggregateFunction, + actualScaleFactor: number | undefined + ): Promise => { + if (actualNamespace !== expectedNamespace) return Promise.reject(); + if (actualType !== expectedType) return Promise.reject(); + if (!isEqual(actualStartDate, expectedStartDate)) return Promise.reject(); + if (!isEqual(actualEndDate, expectedEndDate)) return Promise.reject(); + if (actualAggregateFn !== expectedAggregateFn) return Promise.reject(); + if (actualScaleFactor !== optionalScaleFactor) return Promise.reject(); + return Promise.resolve(result); + } + ); +} + +export function setupMinValueResult( + dailyData: DailyData | DailyDataV2, + result: DailyDataQueryResult, + valueFunctionEvaluator?: (valueFn: DailyDataValueFunction | undefined) => boolean +): void { + setupResult('buildMinValueResult', dailyData, result, valueFunctionEvaluator ?? (valueFn => !valueFn)); +} + export function setupMaxValueResult( dailyData: DailyData | DailyDataV2, result: DailyDataQueryResult, @@ -141,7 +180,7 @@ export function setupMostRecentValueResult( } function setupResult( - functionName: 'buildMaxValueResult' | 'buildTotalValueResult' | 'buildAverageValueResult' | 'buildMostRecentValueResult', + functionName: 'buildMinValueResult' | 'buildMaxValueResult' | 'buildTotalValueResult' | 'buildAverageValueResult' | 'buildMostRecentValueResult', expectedDailyData: DailyData | DailyDataV2, result: DailyDataQueryResult, valueFunctionEvaluator: (valueFn: DailyDataValueFunction | undefined) => boolean diff --git a/__tests__/helpers/daily-data-providers/apple-health-blood-glucose.test.ts b/__tests__/helpers/daily-data-providers/apple-health-blood-glucose.test.ts new file mode 100644 index 000000000..d85913d77 --- /dev/null +++ b/__tests__/helpers/daily-data-providers/apple-health-blood-glucose.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; +import appleHealthBloodGlucose from '../../../src/helpers/daily-data-providers/apple-health-blood-glucose'; + +describe('Daily Data Provider - Apple Health Blood Glucose', () => { + it('Should query for aggregate daily data and return the average values keyed by date.', async () => { + setupAggregateDailyData('AppleHealth', 'Blood Glucose', sampleStartDate, sampleEndDate, 'avg', sampleResult); + expect(await appleHealthBloodGlucose(sampleStartDate, sampleEndDate)).toBe(sampleResult); + }); +}); diff --git a/__tests__/helpers/daily-data-providers/apple-health-heart-rate-range.test.ts b/__tests__/helpers/daily-data-providers/apple-health-heart-rate-range.test.ts index 9af94823a..22a26bfc1 100644 --- a/__tests__/helpers/daily-data-providers/apple-health-heart-rate-range.test.ts +++ b/__tests__/helpers/daily-data-providers/apple-health-heart-rate-range.test.ts @@ -1,36 +1,27 @@ -import { describe, expect, it } from '@jest/globals'; -import { sampleEndDate, sampleStartDate, setupDailyData, startDateFunctionEvaluator } from '../../fixtures/daily-data-providers'; +import { describe, expect, it, jest } from '@jest/globals'; +import { sampleEndDate, sampleStartDate, setupDailyDataProvider } from '../../fixtures/daily-data-providers'; import getDayKey from '../../../src/helpers/get-day-key'; -import { DailyData } from '../../../src/helpers/daily-data-providers/daily-data'; -import { DeviceDataPoint } from '@careevolution/mydatahelps-js'; import appleHealthHeartRateRange from '../../../src/helpers/daily-data-providers/apple-health-heart-rate-range'; +import { appleHealthMaxHeartRateDataProvider, appleHealthMinHeartRateDataProvider } from '../../../src/helpers/daily-data-providers/index'; + +jest.mock('../../../src/helpers/daily-data-providers/index'); describe('Daily Data Provider - Apple Health Heart Rate Range', () => { - it('Should query for daily data and build a result keyed by start date.', async () => { - const dailyData: DailyData = { - [getDayKey(sampleStartDate)]: [ - { type: 'HourlyMaximumHeartRate', value: '120' } as DeviceDataPoint, - { type: 'HourlyMinimumHeartRate', value: '65' } as DeviceDataPoint, - { type: 'HourlyMaximumHeartRate', value: '100' } as DeviceDataPoint, - { type: 'HourlyMinimumHeartRate', value: '60' } as DeviceDataPoint - ] - }; - setupDailyData('AppleHealth', ['HourlyMaximumHeartRate', 'HourlyMinimumHeartRate'], sampleStartDate, sampleEndDate, startDateFunctionEvaluator, dailyData); + it('Should query for daily data and compute ranges by subtracting min from max for each day.', async () => { + const dayKey = getDayKey(sampleStartDate); + setupDailyDataProvider(appleHealthMinHeartRateDataProvider as jest.Mock, sampleStartDate, sampleEndDate, { [dayKey]: 60 }); + setupDailyDataProvider(appleHealthMaxHeartRateDataProvider as jest.Mock, sampleStartDate, sampleEndDate, { [dayKey]: 110 }); const result = await appleHealthHeartRateRange(sampleStartDate, sampleEndDate); expect(Object.keys(result)).toHaveLength(1); - expect(result[getDayKey(sampleStartDate)]).toBe(60); + expect(result[dayKey]).toBe(50); }); it('Should exclude days that do not have a minimum value.', async () => { - const dailyData: DailyData = { - [getDayKey(sampleStartDate)]: [ - { type: 'HourlyMaximumHeartRate', value: '120' } as DeviceDataPoint, - { type: 'HourlyMaximumHeartRate', value: '100' } as DeviceDataPoint, - ] - }; - setupDailyData('AppleHealth', ['HourlyMaximumHeartRate', 'HourlyMinimumHeartRate'], sampleStartDate, sampleEndDate, startDateFunctionEvaluator, dailyData); + const dayKey = getDayKey(sampleStartDate); + setupDailyDataProvider(appleHealthMinHeartRateDataProvider as jest.Mock, sampleStartDate, sampleEndDate, {}); + setupDailyDataProvider(appleHealthMaxHeartRateDataProvider as jest.Mock, sampleStartDate, sampleEndDate, { [dayKey]: 110 }); const result = await appleHealthHeartRateRange(sampleStartDate, sampleEndDate); @@ -38,13 +29,9 @@ describe('Daily Data Provider - Apple Health Heart Rate Range', () => { }); it('Should exclude days that do not have a maximum value.', async () => { - const dailyData: DailyData = { - [getDayKey(sampleStartDate)]: [ - { type: 'HourlyMinimumHeartRate', value: '65' } as DeviceDataPoint, - { type: 'HourlyMinimumHeartRate', value: '60' } as DeviceDataPoint - ] - }; - setupDailyData('AppleHealth', ['HourlyMaximumHeartRate', 'HourlyMinimumHeartRate'], sampleStartDate, sampleEndDate, startDateFunctionEvaluator, dailyData); + const dayKey = getDayKey(sampleStartDate); + setupDailyDataProvider(appleHealthMinHeartRateDataProvider as jest.Mock, sampleStartDate, sampleEndDate, { [dayKey]: 60 }); + setupDailyDataProvider(appleHealthMaxHeartRateDataProvider as jest.Mock, sampleStartDate, sampleEndDate, {}); const result = await appleHealthHeartRateRange(sampleStartDate, sampleEndDate); diff --git a/__tests__/helpers/daily-data-providers/apple-health-max-blood-glucose.test.ts b/__tests__/helpers/daily-data-providers/apple-health-max-blood-glucose.test.ts new file mode 100644 index 000000000..b9ca2ee68 --- /dev/null +++ b/__tests__/helpers/daily-data-providers/apple-health-max-blood-glucose.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; +import appleHealthMaxBloodGlucose from '../../../src/helpers/daily-data-providers/apple-health-max-blood-glucose'; + +describe('Daily Data Provider - Apple Health Max Blood Glucose', () => { + it('Should query for aggregate daily data and return the maximum values keyed by date.', async () => { + setupAggregateDailyData('AppleHealth', 'Blood Glucose', sampleStartDate, sampleEndDate, 'max', sampleResult); + expect(await appleHealthMaxBloodGlucose(sampleStartDate, sampleEndDate)).toBe(sampleResult); + }); +}); diff --git a/__tests__/helpers/daily-data-providers/apple-health-max-heart-rate.test.ts b/__tests__/helpers/daily-data-providers/apple-health-max-heart-rate.test.ts index a1736ddac..dd38e7f0d 100644 --- a/__tests__/helpers/daily-data-providers/apple-health-max-heart-rate.test.ts +++ b/__tests__/helpers/daily-data-providers/apple-health-max-heart-rate.test.ts @@ -1,11 +1,10 @@ import { describe, expect, it } from '@jest/globals'; -import { sampleDailyData, sampleEndDate, sampleResult, sampleStartDate, setupDailyData, setupMaxValueResult, startDateFunctionEvaluator } from '../../fixtures/daily-data-providers'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; import appleHealthMaxHeartRate from '../../../src/helpers/daily-data-providers/apple-health-max-heart-rate'; describe('Daily Data Provider - Apple Health Max Heart Rate', () => { - it('Should query for daily data and build a max value result keyed by start date.', async () => { - setupDailyData('AppleHealth', 'HourlyMaximumHeartRate', sampleStartDate, sampleEndDate, startDateFunctionEvaluator, sampleDailyData); - setupMaxValueResult(sampleDailyData, sampleResult); + it('Should query for aggregate daily data and return the maximum values keyed by date.', async () => { + setupAggregateDailyData('AppleHealth', 'Hourly Maximum Heart Rate', sampleStartDate, sampleEndDate, 'max', sampleResult); expect(await appleHealthMaxHeartRate(sampleStartDate, sampleEndDate)).toBe(sampleResult); }); }); diff --git a/__tests__/helpers/daily-data-providers/apple-health-min-blood-glucose.test.ts b/__tests__/helpers/daily-data-providers/apple-health-min-blood-glucose.test.ts new file mode 100644 index 000000000..2cb1d73d6 --- /dev/null +++ b/__tests__/helpers/daily-data-providers/apple-health-min-blood-glucose.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; +import appleHealthMinBloodGlucose from '../../../src/helpers/daily-data-providers/apple-health-min-blood-glucose'; + +describe('Daily Data Provider - Apple Health Min Blood Glucose', () => { + it('Should query for aggregate daily data and return the minimum values keyed by date.', async () => { + setupAggregateDailyData('AppleHealth', 'Blood Glucose', sampleStartDate, sampleEndDate, 'min', sampleResult); + expect(await appleHealthMinBloodGlucose(sampleStartDate, sampleEndDate)).toBe(sampleResult); + }); +}); diff --git a/__tests__/helpers/daily-data-providers/apple-health-min-heart-rate.test.ts b/__tests__/helpers/daily-data-providers/apple-health-min-heart-rate.test.ts new file mode 100644 index 000000000..2e24c7f3e --- /dev/null +++ b/__tests__/helpers/daily-data-providers/apple-health-min-heart-rate.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; +import appleHealthMinHeartRate from '../../../src/helpers/daily-data-providers/apple-health-min-heart-rate'; + +describe('Daily Data Provider - Apple Health Min Heart Rate', () => { + it('Should query for aggregate daily data and return the minimum values keyed by date.', async () => { + setupAggregateDailyData('AppleHealth', 'Hourly Minimum Heart Rate', sampleStartDate, sampleEndDate, 'min', sampleResult); + expect(await appleHealthMinHeartRate(sampleStartDate, sampleEndDate)).toBe(sampleResult); + }); +}); diff --git a/__tests__/helpers/daily-data-providers/apple-health-mindful-minutes.test.ts b/__tests__/helpers/daily-data-providers/apple-health-mindful-minutes.test.ts index ad0f9ed08..4c273a249 100644 --- a/__tests__/helpers/daily-data-providers/apple-health-mindful-minutes.test.ts +++ b/__tests__/helpers/daily-data-providers/apple-health-mindful-minutes.test.ts @@ -1,16 +1,16 @@ import { describe, expect, it } from '@jest/globals'; -import { sampleEndDate, sampleResult, sampleStartDate, sampleTimeRanges, setupDailyDataPoints, setupDailyTimeRanges, setupMinutesResult } from '../../fixtures/daily-data-providers'; -import { DeviceDataPoint } from '@careevolution/mydatahelps-js'; +import { sampleEndDate, sampleResult, sampleStartDate, sampleTimeRanges, setupDailyDataPointsV2, setupDailyTimeRanges, setupMinutesResult } from '../../fixtures/daily-data-providers'; +import { DeviceDataV2Point } from '@careevolution/mydatahelps-js'; import * as mindfulTherapyFunctions from '../../../src/helpers/daily-data-providers/common-mindful-and-therapy'; import appleHealthMindfulMinutes from '../../../src/helpers/daily-data-providers/apple-health-mindful-minutes'; describe('Daily Data Provider - Apple Health Mindful Minutes', () => { it('Should query for daily data points, filter out therapy data points, and build a minutes result.', async () => { - const mindfulDataPoint: DeviceDataPoint = { identifier: 'Mindful' } as DeviceDataPoint; - const therapyDataPoint: DeviceDataPoint = { identifier: 'Therapy' } as DeviceDataPoint; + const mindfulDataPoint = { identifier: 'Mindful' } as DeviceDataV2Point; + const therapyDataPoint = { identifier: 'Therapy' } as DeviceDataV2Point; jest.spyOn(mindfulTherapyFunctions, 'isSilverCloudCbtDataPoint').mockImplementation(dataPoint => dataPoint === therapyDataPoint); - setupDailyDataPoints('AppleHealth', 'MindfulSession', sampleStartDate, sampleEndDate, undefined, [mindfulDataPoint, therapyDataPoint]); + setupDailyDataPointsV2('AppleHealth', 'Mindful Sessions', sampleStartDate, sampleEndDate, undefined, undefined, [mindfulDataPoint, therapyDataPoint]); setupDailyTimeRanges([mindfulDataPoint], sampleTimeRanges); setupMinutesResult(sampleStartDate, sampleEndDate, sampleTimeRanges, sampleResult); diff --git a/__tests__/helpers/daily-data-providers/apple-health-resting-heart-rate.test.ts b/__tests__/helpers/daily-data-providers/apple-health-resting-heart-rate.test.ts index 25c22a9f4..8d9cc8663 100644 --- a/__tests__/helpers/daily-data-providers/apple-health-resting-heart-rate.test.ts +++ b/__tests__/helpers/daily-data-providers/apple-health-resting-heart-rate.test.ts @@ -1,11 +1,10 @@ import { describe, expect, it } from '@jest/globals'; -import { sampleDailyData, sampleEndDate, sampleResult, sampleStartDate, setupAverageValueResult, setupDailyData, startDateFunctionEvaluator } from '../../fixtures/daily-data-providers'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; import appleHealthRestingHeartRate from '../../../src/helpers/daily-data-providers/apple-health-resting-heart-rate'; describe('Daily Data Provider - Apple Health Resting Heart Rate', () => { - it('Should query for daily data and build an average value result keyed by start date.', async () => { - setupDailyData('AppleHealth', 'RestingHeartRate', sampleStartDate, sampleEndDate, startDateFunctionEvaluator, sampleDailyData); - setupAverageValueResult(sampleDailyData, sampleResult); + it('Should query for aggregate daily data and return the average values keyed by date.', async () => { + setupAggregateDailyData('AppleHealth', 'Resting Heart Rate', sampleStartDate, sampleEndDate, 'avg', sampleResult); expect(await appleHealthRestingHeartRate(sampleStartDate, sampleEndDate)).toBe(sampleResult); }); }); diff --git a/__tests__/helpers/daily-data-providers/apple-health-sleep-v2.test.ts b/__tests__/helpers/daily-data-providers/apple-health-sleep-v2.test.ts deleted file mode 100644 index 63531e1ee..000000000 --- a/__tests__/helpers/daily-data-providers/apple-health-sleep-v2.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, test } from '@jest/globals'; -import { sampleEndDate, sampleResult, sampleStartDate, sampleTimeRanges, setupDailyDataPointsV2, setupDailyTimeRanges, setupMinutesResult } from '../../fixtures/daily-data-providers'; -import { DeviceDataV2Point } from '@careevolution/mydatahelps-js'; -import { coreSleepMinutes, deepSleepMinutes, remSleepMinutes, totalSleepMinutes } from '../../../src/helpers/daily-data-providers/apple-health-sleep-v2'; - -describe('Daily Data Provider - Apple Health Sleep V2', () => { - test.each([ - { title: 'Core', sleepFunction: coreSleepMinutes, levels: ['AsleepCore'] }, - { title: 'REM', sleepFunction: remSleepMinutes, levels: ['AsleepREM'] }, - { title: 'Deep', sleepFunction: deepSleepMinutes, levels: ['AsleepDeep'] }, - { title: 'Total', sleepFunction: totalSleepMinutes, levels: ['AsleepCore', 'AsleepREM', 'AsleepDeep', 'Asleep'] } - ])('$title - Should query for daily data points, filter by sleep level, and build a minutes result.', async ({ sleepFunction, levels }) => { - const dataPoints: DeviceDataV2Point[] = [{ value: 'Other' } as DeviceDataV2Point]; - levels.forEach(level => { - dataPoints.push({ value: level } as DeviceDataV2Point); - }); - - setupDailyDataPointsV2('AppleHealth', 'Sleep Analysis', sampleStartDate, sampleEndDate, undefined, undefined, dataPoints); - setupDailyTimeRanges(dataPoints.slice(1), sampleTimeRanges, -6); - setupMinutesResult(sampleStartDate, sampleEndDate, sampleTimeRanges, sampleResult); - - expect(await sleepFunction(sampleStartDate, sampleEndDate)).toBe(sampleResult); - }); -}); diff --git a/__tests__/helpers/daily-data-providers/apple-health-sleep.test.ts b/__tests__/helpers/daily-data-providers/apple-health-sleep.test.ts index 49dc92806..2c17c1a3e 100644 --- a/__tests__/helpers/daily-data-providers/apple-health-sleep.test.ts +++ b/__tests__/helpers/daily-data-providers/apple-health-sleep.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from '@jest/globals'; -import { sampleEndDate, sampleResult, sampleStartDate, sampleTimeRanges, setupDailyDataPoints, setupDailyTimeRanges, setupMinutesResult } from '../../fixtures/daily-data-providers'; -import { DeviceDataPoint } from '@careevolution/mydatahelps-js'; +import { sampleEndDate, sampleResult, sampleStartDate, sampleTimeRanges, setupDailyDataPointsV2, setupDailyTimeRanges, setupMinutesResult } from '../../fixtures/daily-data-providers'; +import { DeviceDataV2Point } from '@careevolution/mydatahelps-js'; import { asleepCoreTime, asleepDeepTime, asleepRemTime, asleepTime, inBedTime } from '../../../src/helpers/daily-data-providers/apple-health-sleep'; describe('Daily Data Provider - Apple Health Sleep', () => { @@ -11,12 +11,12 @@ describe('Daily Data Provider - Apple Health Sleep', () => { { title: 'Deep', sleepFunction: asleepDeepTime, sleepTypes: ['AsleepDeep'] }, { title: 'Total', sleepFunction: asleepTime, sleepTypes: ['AsleepCore', 'AsleepREM', 'AsleepDeep', 'Asleep'] } ])('$title - Should query for daily data points, filter by sleep type, and build a minutes result.', async ({ sleepFunction, sleepTypes }) => { - const dataPoints: DeviceDataPoint[] = [{ value: 'Other' } as DeviceDataPoint]; + const dataPoints = [{ value: 'Other' } as DeviceDataV2Point]; sleepTypes.forEach(sleepType => { - dataPoints.push({ value: sleepType } as DeviceDataPoint); + dataPoints.push({ value: sleepType } as DeviceDataV2Point); }); - setupDailyDataPoints('AppleHealth', 'SleepAnalysisInterval', sampleStartDate, sampleEndDate, undefined, dataPoints); + setupDailyDataPointsV2('AppleHealth', 'Sleep Analysis', sampleStartDate, sampleEndDate, undefined, undefined, dataPoints); setupDailyTimeRanges(dataPoints.slice(1), sampleTimeRanges, -6); setupMinutesResult(sampleStartDate, sampleEndDate, sampleTimeRanges, sampleResult); diff --git a/__tests__/helpers/daily-data-providers/apple-health-steps-while-wearing-device.test.ts b/__tests__/helpers/daily-data-providers/apple-health-steps-while-wearing-device.test.ts index 59be4b000..4b8379235 100644 --- a/__tests__/helpers/daily-data-providers/apple-health-steps-while-wearing-device.test.ts +++ b/__tests__/helpers/daily-data-providers/apple-health-steps-while-wearing-device.test.ts @@ -1,33 +1,23 @@ import { describe, expect, it } from '@jest/globals'; -import { sampleEndDate, sampleResult, sampleStartDate, setupDailyData, setupTotalValueResult, startDateFunctionEvaluator } from '../../fixtures/daily-data-providers'; -import { DailyData } from '../../../src/helpers/daily-data-providers/daily-data'; +import { sampleEndDate, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; import getDayKey from '../../../src/helpers/get-day-key'; -import { DeviceDataPoint, DeviceDataV2Aggregate, DeviceDataV2AggregateQuery } from '@careevolution/mydatahelps-js'; +import { DeviceDataV2Aggregate, DeviceDataV2AggregateQuery } from '@careevolution/mydatahelps-js'; import { add, format } from 'date-fns'; import * as queryAllDeviceDataV2AggregatesModule from '../../../src/helpers/query-all-device-data-v2-aggregates'; import appleHealthStepsWhileWearingDevice from '../../../src/helpers/daily-data-providers/apple-health-steps-while-wearing-device'; +import { DailyDataQueryResult } from '../../../src'; describe('Daily Data Provider - Apple Health Steps While Wearing Device', () => { - it('Should query for daily data and build a total value result keyed by start date.', async () => { - const dailyData: DailyData = { - [getDayKey(sampleStartDate)]: [ - { identifier: 'Watch Date' } as DeviceDataPoint - ], - [getDayKey(add(sampleStartDate, { days: 1 }))]: [ - { identifier: 'Oura Date' } as DeviceDataPoint - ], - [getDayKey(sampleEndDate)]: [ - { identifier: 'Other Date' } as DeviceDataPoint - ] + it('Should query for aggregate daily data and return the sum of values keyed by date.', async () => { + const result: DailyDataQueryResult = { + [getDayKey(sampleStartDate)]: 500, + [getDayKey(add(sampleStartDate, { days: 1 }))]: 600, + [getDayKey(sampleEndDate)]: 400 }; - const filteredDailyData: DailyData = { - [getDayKey(sampleStartDate)]: [ - { identifier: 'Watch Date' } as DeviceDataPoint - ], - [getDayKey(add(sampleStartDate, { days: 1 }))]: [ - { identifier: 'Oura Date' } as DeviceDataPoint - ] + const filteredResult: DailyDataQueryResult = { + [getDayKey(sampleStartDate)]: 500, + [getDayKey(add(sampleStartDate, { days: 1 }))]: 600 }; jest.spyOn(queryAllDeviceDataV2AggregatesModule, 'default').mockImplementation((query: DeviceDataV2AggregateQuery): Promise => { @@ -52,8 +42,7 @@ describe('Daily Data Provider - Apple Health Steps While Wearing Device', () => } ] as DeviceDataV2Aggregate[]); }); - setupDailyData('AppleHealth', 'HourlySteps', sampleStartDate, sampleEndDate, startDateFunctionEvaluator, dailyData); - setupTotalValueResult(filteredDailyData, sampleResult); - expect(await appleHealthStepsWhileWearingDevice(sampleStartDate, sampleEndDate)).toBe(sampleResult); + setupAggregateDailyData('AppleHealth', 'Hourly Steps', sampleStartDate, sampleEndDate, 'sum', result); + expect(await appleHealthStepsWhileWearingDevice(sampleStartDate, sampleEndDate)).toEqual(filteredResult); }); }); diff --git a/__tests__/helpers/daily-data-providers/apple-health-steps.test.ts b/__tests__/helpers/daily-data-providers/apple-health-steps.test.ts index 17e45b393..3786dad1f 100644 --- a/__tests__/helpers/daily-data-providers/apple-health-steps.test.ts +++ b/__tests__/helpers/daily-data-providers/apple-health-steps.test.ts @@ -1,11 +1,10 @@ import { describe, expect, it } from '@jest/globals'; -import { sampleDailyData, sampleEndDate, sampleResult, sampleStartDate, setupDailyData, setupTotalValueResult, startDateFunctionEvaluator } from '../../fixtures/daily-data-providers'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; import appleHealthSteps from '../../../src/helpers/daily-data-providers/apple-health-steps'; describe('Daily Data Provider - Apple Health Steps', () => { - it('Should query for daily data and build a total value result keyed by start date.', async () => { - setupDailyData('AppleHealth', 'HourlySteps', sampleStartDate, sampleEndDate, startDateFunctionEvaluator, sampleDailyData); - setupTotalValueResult(sampleDailyData, sampleResult); + it('Should query for aggregate daily data and return the sum of values keyed by date.', async () => { + setupAggregateDailyData('AppleHealth', 'Hourly Steps', sampleStartDate, sampleEndDate, 'sum', sampleResult); expect(await appleHealthSteps(sampleStartDate, sampleEndDate)).toBe(sampleResult); }); }); diff --git a/__tests__/helpers/daily-data-providers/combined-blood-glucose.test.ts b/__tests__/helpers/daily-data-providers/combined-blood-glucose.test.ts new file mode 100644 index 000000000..cbf503027 --- /dev/null +++ b/__tests__/helpers/daily-data-providers/combined-blood-glucose.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from '@jest/globals'; +import { createEmptyCombinedDataCollectionSettings, createMockResult, sampleEndDate, sampleResult, sampleStartDate, setupCombinedDataCollectionSettings, setupCombinedFirstValueResult, setupDailyDataProvider } from '../../fixtures/daily-data-providers'; +import combinedBloodGlucose from '../../../src/helpers/daily-data-providers/combined-blood-glucose'; +import * as dailyDataResultFunctions from '../../../src/helpers/daily-data-providers/daily-data/daily-data-result'; +import { appleHealthBloodGlucoseDataProvider, healthConnectBloodGlucoseDataProvider } from '../../../src/helpers/daily-data-providers'; + +jest.mock('../../../src/helpers/daily-data-providers/apple-health-blood-glucose', () => ({ + __esModule: true, + default: jest.fn() +})); + +jest.mock('../../../src/helpers/daily-data-providers/health-connect-blood-glucose', () => ({ + __esModule: true, + default: jest.fn() +})); + +describe('Daily Data Provider - Combined Blood Glucose', () => { + + const appleHealthBloodGlucoseDataProviderMock = appleHealthBloodGlucoseDataProvider as jest.Mock; + const healthConnectBloodGlucoseDataProviderMock = healthConnectBloodGlucoseDataProvider as jest.Mock; + const combinedFirstValueResultMock = jest.spyOn(dailyDataResultFunctions, 'combineResultsUsingFirstValue'); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('Should return an empty result when no providers are enabled.', async () => { + const combinedSettings = createEmptyCombinedDataCollectionSettings(); + + setupCombinedDataCollectionSettings(true, combinedSettings); + + const result = await combinedBloodGlucose(sampleStartDate, sampleEndDate); + + expect(result).toEqual({}); + expect(appleHealthBloodGlucoseDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectBloodGlucoseDataProviderMock).not.toHaveBeenCalled(); + expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); + }); + + it('Should return an empty result when providers are enabled, but not the correct data types.', async () => { + const combinedSettings = createEmptyCombinedDataCollectionSettings(); + combinedSettings.settings.appleHealthEnabled = true; + combinedSettings.settings.healthConnectEnabled = true; + + setupCombinedDataCollectionSettings(true, combinedSettings); + + const result = await combinedBloodGlucose(sampleStartDate, sampleEndDate); + + expect(result).toEqual({}); + expect(appleHealthBloodGlucoseDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectBloodGlucoseDataProviderMock).not.toHaveBeenCalled(); + expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); + }); + + it('Should return the Apple Health result when fully enabled.', async () => { + const combinedSettings = createEmptyCombinedDataCollectionSettings(); + combinedSettings.settings.appleHealthEnabled = true; + combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Blood Glucose', enabled: true } + ); + + const appleHealthResult = createMockResult(); + + setupCombinedDataCollectionSettings(true, combinedSettings); + setupDailyDataProvider(appleHealthBloodGlucoseDataProviderMock, sampleStartDate, sampleEndDate, appleHealthResult); + + const result = await combinedBloodGlucose(sampleStartDate, sampleEndDate); + + expect(result).toBe(appleHealthResult); + expect(healthConnectBloodGlucoseDataProviderMock).not.toHaveBeenCalled(); + expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); + }); + + it('Should return the Health Connect result when fully enabled.', async () => { + const combinedSettings = createEmptyCombinedDataCollectionSettings(); + combinedSettings.settings.healthConnectEnabled = true; + combinedSettings.deviceDataV2Types.push( + { namespace: 'HealthConnect', type: 'blood-glucose', enabled: true } + ); + + const healthConnectResult = createMockResult(); + + setupCombinedDataCollectionSettings(true, combinedSettings); + setupDailyDataProvider(healthConnectBloodGlucoseDataProviderMock, sampleStartDate, sampleEndDate, healthConnectResult); + + const result = await combinedBloodGlucose(sampleStartDate, sampleEndDate); + + expect(result).toBe(healthConnectResult); + expect(appleHealthBloodGlucoseDataProviderMock).not.toHaveBeenCalled(); + expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); + }); + + it('Should return a combined first-value result when multiple sources are fully enabled.', async () => { + const combinedSettings = createEmptyCombinedDataCollectionSettings(); + combinedSettings.settings.appleHealthEnabled = true; + combinedSettings.settings.healthConnectEnabled = true; + combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Blood Glucose', enabled: true }, + { namespace: 'HealthConnect', type: 'blood-glucose', enabled: true } + ); + + const appleHealthResult = createMockResult(); + const healthConnectResult = createMockResult(); + + setupCombinedDataCollectionSettings(true, combinedSettings); + setupDailyDataProvider(appleHealthBloodGlucoseDataProviderMock, sampleStartDate, sampleEndDate, appleHealthResult); + setupDailyDataProvider(healthConnectBloodGlucoseDataProviderMock, sampleStartDate, sampleEndDate, healthConnectResult); + setupCombinedFirstValueResult(sampleStartDate, sampleEndDate, [appleHealthResult, healthConnectResult], sampleResult); + + const result = await combinedBloodGlucose(sampleStartDate, sampleEndDate); + + expect(result).toBe(sampleResult); + }); +}); diff --git a/__tests__/helpers/daily-data-providers/combined-mindful-minutes.test.ts b/__tests__/helpers/daily-data-providers/combined-mindful-minutes.test.ts index 59682a169..332cb43ba 100644 --- a/__tests__/helpers/daily-data-providers/combined-mindful-minutes.test.ts +++ b/__tests__/helpers/daily-data-providers/combined-mindful-minutes.test.ts @@ -64,8 +64,8 @@ describe('Daily Data Provider - Combined Mindful Minutes', () => { it('Should return the Apple Health result when fully enabled.', async () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); combinedSettings.settings.appleHealthEnabled = true; - combinedSettings.settings.queryableDeviceDataTypes.push( - { namespace: 'AppleHealth', type: 'MindfulSession' } + combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Mindful Sessions', enabled: true } ); const appleHealthResult = createMockResult(); @@ -121,16 +121,16 @@ describe('Daily Data Provider - Combined Mindful Minutes', () => { expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); }); - it('Should return a combined first-value result when both sources are fully enabled.', async () => { + it('Should return a combined first-value result when multiple sources are fully enabled.', async () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); combinedSettings.settings.appleHealthEnabled = true; combinedSettings.settings.googleFitEnabled = true; + combinedSettings.settings.healthConnectEnabled = true; combinedSettings.settings.queryableDeviceDataTypes.push( - { namespace: 'AppleHealth', type: 'MindfulSession' }, { namespace: 'GoogleFit', type: 'ActivitySegment' } ); - combinedSettings.settings.healthConnectEnabled = true; combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Mindful Sessions', enabled: true }, { namespace: 'HealthConnect', type: 'mindfulness-sessions', enabled: true } ); diff --git a/__tests__/helpers/daily-data-providers/combined-resting-heart-rate.test.ts b/__tests__/helpers/daily-data-providers/combined-resting-heart-rate.test.ts index 0fb6d6c50..e08a0b4f8 100644 --- a/__tests__/helpers/daily-data-providers/combined-resting-heart-rate.test.ts +++ b/__tests__/helpers/daily-data-providers/combined-resting-heart-rate.test.ts @@ -118,8 +118,8 @@ describe('Daily Data Provider - Combined Resting Heart Rate', () => { it('Should return the Apple Health result when fully enabled.', async () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); combinedSettings.settings.appleHealthEnabled = true; - combinedSettings.settings.queryableDeviceDataTypes.push( - { namespace: 'AppleHealth', type: 'RestingHeartRate' } + combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Resting Heart Rate', enabled: true } ); const appleHealthResult = createMockResult(); @@ -188,10 +188,8 @@ describe('Daily Data Provider - Combined Resting Heart Rate', () => { combinedSettings.settings.appleHealthEnabled = true; combinedSettings.settings.healthConnectEnabled = true; combinedSettings.settings.ouraEnabled = true; - combinedSettings.settings.queryableDeviceDataTypes.push( - { namespace: 'AppleHealth', type: 'RestingHeartRate' } - ); combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Resting Heart Rate', enabled: true }, { namespace: 'HealthConnect', type: 'resting-heart-rate', enabled: true }, { namespace: 'Oura', type: 'sleep', enabled: true } ); diff --git a/__tests__/helpers/daily-data-providers/combined-sleep.test.ts b/__tests__/helpers/daily-data-providers/combined-sleep.test.ts index e91cbfda9..344a7b001 100644 --- a/__tests__/helpers/daily-data-providers/combined-sleep.test.ts +++ b/__tests__/helpers/daily-data-providers/combined-sleep.test.ts @@ -118,8 +118,8 @@ describe('Daily Data Provider - Combined Sleep', () => { it('Should return the Apple Health result when fully enabled.', async () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); combinedSettings.settings.appleHealthEnabled = true; - combinedSettings.settings.queryableDeviceDataTypes.push( - { namespace: 'AppleHealth', type: 'SleepAnalysisInterval' } + combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Sleep Analysis', enabled: true } ); const appleHealthResult = createMockResult(); @@ -188,8 +188,8 @@ describe('Daily Data Provider - Combined Sleep', () => { combinedSettings.settings.appleHealthEnabled = true; combinedSettings.settings.healthConnectEnabled = true; combinedSettings.settings.ouraEnabled = true; - combinedSettings.settings.queryableDeviceDataTypes.push( - { namespace: 'AppleHealth', type: 'SleepAnalysisInterval' } + combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Sleep Analysis', enabled: true } ); combinedSettings.deviceDataV2Types.push( { namespace: 'HealthConnect', type: 'sleep', enabled: true }, diff --git a/__tests__/helpers/daily-data-providers/combined-steps.test.ts b/__tests__/helpers/daily-data-providers/combined-steps.test.ts index 729c700ef..596a35a54 100644 --- a/__tests__/helpers/daily-data-providers/combined-steps.test.ts +++ b/__tests__/helpers/daily-data-providers/combined-steps.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from '@jest/globals'; import { createEmptyCombinedDataCollectionSettings, createMockResult, sampleEndDate, sampleResult, sampleStartDate, setupCombinedDataCollectionSettings, setupCombinedMaxValueResult, setupDailyDataProvider } from '../../fixtures/daily-data-providers'; import combinedSteps from '../../../src/helpers/daily-data-providers/combined-steps'; import * as dailyDataResultFunctions from '../../../src/helpers/daily-data-providers/daily-data/daily-data-result'; -import { appleHealthStepsDataProvider, fitbitStepsDataProvider, garminStepsDataProvider, googleFitStepsDataProvider, ouraStepsDataProvider } from '../../../src/helpers/daily-data-providers'; +import { appleHealthStepsDataProvider, fitbitStepsDataProvider, garminStepsDataProvider, googleFitStepsDataProvider, healthConnectStepsDataProvider, ouraStepsDataProvider } from '../../../src/helpers/daily-data-providers'; jest.mock('../../../src/helpers/daily-data-providers/fitbit-steps', () => ({ __esModule: true, @@ -19,6 +19,11 @@ jest.mock('../../../src/helpers/daily-data-providers/apple-health-steps', () => default: jest.fn() })); +jest.mock('../../../src/helpers/daily-data-providers/health-connect-steps', () => ({ + __esModule: true, + default: jest.fn() +})); + jest.mock('../../../src/helpers/daily-data-providers/google-fit-steps', () => ({ __esModule: true, default: jest.fn() @@ -34,6 +39,7 @@ describe('Daily Data Provider - Combined Steps', () => { const fitbitStepsDataProviderMock = fitbitStepsDataProvider as jest.Mock; const garminStepsDataProviderMock = garminStepsDataProvider as jest.Mock; const appleHealthStepsDataProviderMock = appleHealthStepsDataProvider as jest.Mock; + const healthConnectStepsDataProviderMock = healthConnectStepsDataProvider as jest.Mock; const googleFitStepsDataProviderMock = googleFitStepsDataProvider as jest.Mock; const ouraStepsDataProviderMock = ouraStepsDataProvider as jest.Mock; const combinedMaxValueResultMock = jest.spyOn(dailyDataResultFunctions, 'combineResultsUsingMaxValue'); @@ -53,6 +59,7 @@ describe('Daily Data Provider - Combined Steps', () => { expect(fitbitStepsDataProviderMock).not.toHaveBeenCalled(); expect(garminStepsDataProviderMock).not.toHaveBeenCalled(); expect(appleHealthStepsDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectStepsDataProviderMock).not.toHaveBeenCalled(); expect(googleFitStepsDataProviderMock).not.toHaveBeenCalled(); expect(ouraStepsDataProviderMock).not.toHaveBeenCalled(); expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); @@ -61,6 +68,7 @@ describe('Daily Data Provider - Combined Steps', () => { it('Should return an empty result when providers are enabled, but not the correct data types.', async () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); combinedSettings.settings.appleHealthEnabled = true; + combinedSettings.settings.healthConnectEnabled = true; combinedSettings.settings.googleFitEnabled = true; combinedSettings.settings.ouraEnabled = true; @@ -72,6 +80,7 @@ describe('Daily Data Provider - Combined Steps', () => { expect(fitbitStepsDataProviderMock).not.toHaveBeenCalled(); expect(garminStepsDataProviderMock).not.toHaveBeenCalled(); expect(appleHealthStepsDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectStepsDataProviderMock).not.toHaveBeenCalled(); expect(googleFitStepsDataProviderMock).not.toHaveBeenCalled(); expect(ouraStepsDataProviderMock).not.toHaveBeenCalled(); expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); @@ -92,6 +101,7 @@ describe('Daily Data Provider - Combined Steps', () => { expect(fitbitStepsDataProviderMock).not.toHaveBeenCalled(); expect(garminStepsDataProviderMock).not.toHaveBeenCalled(); expect(appleHealthStepsDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectStepsDataProviderMock).not.toHaveBeenCalled(); expect(googleFitStepsDataProviderMock).not.toHaveBeenCalled(); expect(ouraStepsDataProviderMock).not.toHaveBeenCalled(); expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); @@ -111,6 +121,7 @@ describe('Daily Data Provider - Combined Steps', () => { expect(result).toBe(fitbitResult); expect(garminStepsDataProviderMock).not.toHaveBeenCalled(); expect(appleHealthStepsDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectStepsDataProviderMock).not.toHaveBeenCalled(); expect(googleFitStepsDataProviderMock).not.toHaveBeenCalled(); expect(ouraStepsDataProviderMock).not.toHaveBeenCalled(); expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); @@ -130,6 +141,7 @@ describe('Daily Data Provider - Combined Steps', () => { expect(result).toBe(garminResult); expect(fitbitStepsDataProviderMock).not.toHaveBeenCalled(); expect(appleHealthStepsDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectStepsDataProviderMock).not.toHaveBeenCalled(); expect(googleFitStepsDataProviderMock).not.toHaveBeenCalled(); expect(ouraStepsDataProviderMock).not.toHaveBeenCalled(); expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); @@ -138,8 +150,8 @@ describe('Daily Data Provider - Combined Steps', () => { it('Should return the Apple Health result when fully enabled.', async () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); combinedSettings.settings.appleHealthEnabled = true; - combinedSettings.settings.queryableDeviceDataTypes.push( - { namespace: 'AppleHealth', type: 'HourlySteps' } + combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Hourly Steps', enabled: true } ); const appleHealthResult = createMockResult(); @@ -152,6 +164,30 @@ describe('Daily Data Provider - Combined Steps', () => { expect(result).toBe(appleHealthResult); expect(fitbitStepsDataProviderMock).not.toHaveBeenCalled(); expect(garminStepsDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectStepsDataProviderMock).not.toHaveBeenCalled(); + expect(googleFitStepsDataProviderMock).not.toHaveBeenCalled(); + expect(ouraStepsDataProviderMock).not.toHaveBeenCalled(); + expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); + }); + + it('Should return the Health Connect result when fully enabled.', async () => { + const combinedSettings = createEmptyCombinedDataCollectionSettings(); + combinedSettings.settings.healthConnectEnabled = true; + combinedSettings.deviceDataV2Types.push( + { namespace: 'HealthConnect', type: 'steps-daily', enabled: true } + ); + + const healthConnectResult = createMockResult(); + + setupCombinedDataCollectionSettings(true, combinedSettings); + setupDailyDataProvider(healthConnectStepsDataProviderMock, sampleStartDate, sampleEndDate, healthConnectResult); + + const result = await combinedSteps(sampleStartDate, sampleEndDate); + + expect(result).toBe(healthConnectResult); + expect(fitbitStepsDataProviderMock).not.toHaveBeenCalled(); + expect(garminStepsDataProviderMock).not.toHaveBeenCalled(); + expect(appleHealthStepsDataProviderMock).not.toHaveBeenCalled(); expect(googleFitStepsDataProviderMock).not.toHaveBeenCalled(); expect(ouraStepsDataProviderMock).not.toHaveBeenCalled(); expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); @@ -175,6 +211,7 @@ describe('Daily Data Provider - Combined Steps', () => { expect(fitbitStepsDataProviderMock).not.toHaveBeenCalled(); expect(garminStepsDataProviderMock).not.toHaveBeenCalled(); expect(appleHealthStepsDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectStepsDataProviderMock).not.toHaveBeenCalled(); expect(ouraStepsDataProviderMock).not.toHaveBeenCalled(); expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); }); @@ -197,6 +234,7 @@ describe('Daily Data Provider - Combined Steps', () => { expect(fitbitStepsDataProviderMock).not.toHaveBeenCalled(); expect(garminStepsDataProviderMock).not.toHaveBeenCalled(); expect(appleHealthStepsDataProviderMock).not.toHaveBeenCalled(); + expect(healthConnectStepsDataProviderMock).not.toHaveBeenCalled(); expect(googleFitStepsDataProviderMock).not.toHaveBeenCalled(); expect(combinedMaxValueResultMock).not.toHaveBeenCalled(); }); @@ -206,19 +244,22 @@ describe('Daily Data Provider - Combined Steps', () => { combinedSettings.settings.fitbitEnabled = true; combinedSettings.settings.garminEnabled = true; combinedSettings.settings.appleHealthEnabled = true; + combinedSettings.settings.healthConnectEnabled = true; combinedSettings.settings.googleFitEnabled = true; combinedSettings.settings.ouraEnabled = true; combinedSettings.settings.queryableDeviceDataTypes.push( - { namespace: 'AppleHealth', type: 'HourlySteps' }, { namespace: 'GoogleFit', type: 'Steps' } ); combinedSettings.deviceDataV2Types.push( + { namespace: 'AppleHealth', type: 'Hourly Steps', enabled: true }, + { namespace: 'HealthConnect', type: 'steps-daily', enabled: true }, { namespace: 'Oura', type: 'daily-activity', enabled: true } ); const fitbitResult = createMockResult(); const garminResult = createMockResult(); const appleHealthResult = createMockResult(); + const healthConnectResult = createMockResult(); const googleFitResult = createMockResult(); const ouraResult = createMockResult(); @@ -226,9 +267,10 @@ describe('Daily Data Provider - Combined Steps', () => { setupDailyDataProvider(fitbitStepsDataProviderMock, sampleStartDate, sampleEndDate, fitbitResult); setupDailyDataProvider(garminStepsDataProviderMock, sampleStartDate, sampleEndDate, garminResult); setupDailyDataProvider(appleHealthStepsDataProviderMock, sampleStartDate, sampleEndDate, appleHealthResult); + setupDailyDataProvider(healthConnectStepsDataProviderMock, sampleStartDate, sampleEndDate, healthConnectResult); setupDailyDataProvider(googleFitStepsDataProviderMock, sampleStartDate, sampleEndDate, googleFitResult); setupDailyDataProvider(ouraStepsDataProviderMock, sampleStartDate, sampleEndDate, ouraResult); - setupCombinedMaxValueResult(sampleStartDate, sampleEndDate, [fitbitResult, garminResult, appleHealthResult, googleFitResult, ouraResult], sampleResult); + setupCombinedMaxValueResult(sampleStartDate, sampleEndDate, [fitbitResult, garminResult, appleHealthResult, healthConnectResult, googleFitResult, ouraResult], sampleResult); const result = await combinedSteps(sampleStartDate, sampleEndDate, true); diff --git a/__tests__/helpers/daily-data-providers/combined-therapy-minutes.test.ts b/__tests__/helpers/daily-data-providers/combined-therapy-minutes.test.ts index c74bfe5e0..c9e7ec543 100644 --- a/__tests__/helpers/daily-data-providers/combined-therapy-minutes.test.ts +++ b/__tests__/helpers/daily-data-providers/combined-therapy-minutes.test.ts @@ -121,15 +121,15 @@ describe('Daily Data Provider - Combined Therapy Minutes', () => { expect(combinedFirstValueResultMock).not.toHaveBeenCalled(); }); - it('Should return a combined first-value result when both sources are fully enabled.', async () => { + it('Should return a combined first-value result when multiple sources are fully enabled.', async () => { const combinedSettings = createEmptyCombinedDataCollectionSettings(); combinedSettings.settings.appleHealthEnabled = true; combinedSettings.settings.googleFitEnabled = true; + combinedSettings.settings.healthConnectEnabled = true; combinedSettings.settings.queryableDeviceDataTypes.push( { namespace: 'AppleHealth', type: 'MindfulSession' }, { namespace: 'GoogleFit', type: 'SilverCloudSession' } ); - combinedSettings.settings.healthConnectEnabled = true; combinedSettings.deviceDataV2Types.push( { namespace: 'HealthConnect', type: 'exercise-session', enabled: true } ); diff --git a/__tests__/helpers/daily-data-providers/daily-data/daily-data-aggregate.test.ts b/__tests__/helpers/daily-data-providers/daily-data/daily-data-aggregate.test.ts new file mode 100644 index 000000000..bb7339327 --- /dev/null +++ b/__tests__/helpers/daily-data-providers/daily-data/daily-data-aggregate.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleEndDate, sampleStartDate } from '../../../fixtures/daily-data-providers'; +import { DeviceDataV2Aggregate, DeviceDataV2AggregateQuery } from '@careevolution/mydatahelps-js'; +import { add, format } from 'date-fns'; +import getDayKey from '../../../../src/helpers/get-day-key'; +import * as queryAllDeviceDataV2AggregatesModule from '../../../../src/helpers/query-all-device-data-v2-aggregates'; +import { queryAggregateDailyData } from '../../../../src/helpers/daily-data-providers/daily-data/daily-data-aggregate'; + +describe('Daily Data Aggregate Tests', () => { + + beforeEach(() => { + jest.restoreAllMocks(); + }); + + describe('queryAggregateDailyData', () => { + it('Should query for device data aggregates and return a result with the positive valued aggregates keyed by date.', async () => { + jest.spyOn(queryAllDeviceDataV2AggregatesModule, 'default').mockImplementation((query: DeviceDataV2AggregateQuery): Promise => { + if (query.namespace !== 'AppleHealth') return Promise.reject(); + if (query.type !== 'Blood Glucose') return Promise.reject(); + if (query.observedAfter !== add(sampleStartDate, { days: -1 }).toISOString()) return Promise.reject(); + if (query.observedBefore !== add(sampleEndDate, { days: 1 }).toISOString()) return Promise.reject(); + if (query.intervalAmount !== 1) return Promise.reject(); + if (query.intervalType !== 'Days') return Promise.reject(); + if (query.aggregateFunctions.length !== 1 || query.aggregateFunctions[0] !== 'avg') return Promise.reject(); + + return Promise.resolve([ + { + date: format(sampleStartDate, 'yyyy-MM-dd\'T\'HH:mm:ss'), + statistics: { 'avg': 100 } as { [key: string]: number } + }, + { + date: format(add(sampleStartDate, { days: 3 }), 'yyyy-MM-dd\'T\'HH:mm:ss'), + statistics: { 'avg': 0 } as { [key: string]: number } + }, + { + date: format(sampleEndDate, 'yyyy-MM-dd\'T\'HH:mm:ss'), + statistics: { 'avg': 110 } as { [key: string]: number } + } + ] as DeviceDataV2Aggregate[]); + }); + + const result = await queryAggregateDailyData('AppleHealth', 'Blood Glucose', sampleStartDate, sampleEndDate, 'avg'); + + expect(Object.keys(result)).toHaveLength(2); + expect(result[getDayKey(sampleStartDate)]).toBe(100); + expect(result[getDayKey(sampleEndDate)]).toBe(110); + }); + + it('Should support a scale factor for returned values.', async () => { + jest.spyOn(queryAllDeviceDataV2AggregatesModule, 'default').mockImplementation((query: DeviceDataV2AggregateQuery): Promise => { + if (query.namespace !== 'HealthConnect') return Promise.reject(); + if (query.type !== 'blood-glucose') return Promise.reject(); + if (query.observedAfter !== add(sampleStartDate, { days: -1 }).toISOString()) return Promise.reject(); + if (query.observedBefore !== add(sampleEndDate, { days: 1 }).toISOString()) return Promise.reject(); + if (query.intervalAmount !== 1) return Promise.reject(); + if (query.intervalType !== 'Days') return Promise.reject(); + if (query.aggregateFunctions.length !== 1 || query.aggregateFunctions[0] !== 'avg') return Promise.reject(); + + return Promise.resolve([ + { + date: format(sampleStartDate, 'yyyy-MM-dd\'T\'HH:mm:ss'), + statistics: { 'avg': 100 / 18 } as { [key: string]: number } + }, + { + date: format(add(sampleStartDate, { days: 3 }), 'yyyy-MM-dd\'T\'HH:mm:ss'), + statistics: { 'avg': 0 } as { [key: string]: number } + }, + { + date: format(sampleEndDate, 'yyyy-MM-dd\'T\'HH:mm:ss'), + statistics: { 'avg': 110 / 18 } as { [key: string]: number } + } + ] as DeviceDataV2Aggregate[]); + }); + + const result = await queryAggregateDailyData('HealthConnect', 'blood-glucose', sampleStartDate, sampleEndDate, 'avg', 18); + + expect(Object.keys(result)).toHaveLength(2); + expect(result[getDayKey(sampleStartDate)]).toBe(100); + expect(result[getDayKey(sampleEndDate)]).toBe(110); + }); + }); +}); diff --git a/__tests__/helpers/daily-data-providers/health-connect-blood-glucose.test.ts b/__tests__/helpers/daily-data-providers/health-connect-blood-glucose.test.ts new file mode 100644 index 000000000..332392958 --- /dev/null +++ b/__tests__/helpers/daily-data-providers/health-connect-blood-glucose.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; +import healthConnectBloodGlucose from '../../../src/helpers/daily-data-providers/health-connect-blood-glucose'; + +describe('Daily Data Provider - Health Connect Blood Glucose', () => { + it('Should query for aggregate daily data and return the average values keyed by date.', async () => { + setupAggregateDailyData('HealthConnect', 'blood-glucose', sampleStartDate, sampleEndDate, 'avg', sampleResult, 18); + expect(await healthConnectBloodGlucose(sampleStartDate, sampleEndDate)).toBe(sampleResult); + }); +}); diff --git a/__tests__/helpers/daily-data-providers/health-connect-max-blood-glucose.test.ts b/__tests__/helpers/daily-data-providers/health-connect-max-blood-glucose.test.ts new file mode 100644 index 000000000..f79282924 --- /dev/null +++ b/__tests__/helpers/daily-data-providers/health-connect-max-blood-glucose.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; +import healthConnectMaxBloodGlucose from '../../../src/helpers/daily-data-providers/health-connect-max-blood-glucose'; + +describe('Daily Data Provider - Health Connect Max Blood Glucose', () => { + it('Should query for aggregate daily data and return the maximum values keyed by date.', async () => { + setupAggregateDailyData('HealthConnect', 'blood-glucose', sampleStartDate, sampleEndDate, 'max', sampleResult, 18); + expect(await healthConnectMaxBloodGlucose(sampleStartDate, sampleEndDate)).toBe(sampleResult); + }); +}); diff --git a/__tests__/helpers/daily-data-providers/health-connect-min-blood-glucose.test.ts b/__tests__/helpers/daily-data-providers/health-connect-min-blood-glucose.test.ts new file mode 100644 index 000000000..4629946ef --- /dev/null +++ b/__tests__/helpers/daily-data-providers/health-connect-min-blood-glucose.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from '@jest/globals'; +import { sampleEndDate, sampleResult, sampleStartDate, setupAggregateDailyData } from '../../fixtures/daily-data-providers'; +import healthConnectMinBloodGlucose from '../../../src/helpers/daily-data-providers/health-connect-min-blood-glucose'; + +describe('Daily Data Provider - Health Connect Min Blood Glucose', () => { + it('Should query for aggregate daily data and return the minimum values keyed by date.', async () => { + setupAggregateDailyData('HealthConnect', 'blood-glucose', sampleStartDate, sampleEndDate, 'min', sampleResult, 18); + expect(await healthConnectMinBloodGlucose(sampleStartDate, sampleEndDate)).toBe(sampleResult); + }); +}); diff --git a/locales/de.json b/locales/de.json index 7c5ddcfb8..518ad45ec 100644 --- a/locales/de.json +++ b/locales/de.json @@ -332,6 +332,9 @@ "average-stress-level": "Durchschnittliche Stressstufe", "awake-time": "Wachzeit", "back": "Zurück", + "blood-glucose": "Blutzucker", + "blood-glucose-max": "Maximaler Blutzucker", + "blood-glucose-min": "Mindestblutzucker", "blood-pressure-reading-classification-elevated": "Erhöht", "blood-pressure-reading-classification-hypertension-stage-1": "Hypertonie Stufe 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hypertonie Stufe 2", diff --git a/locales/en.json b/locales/en.json index 8c21ba51a..85ee85084 100644 --- a/locales/en.json +++ b/locales/en.json @@ -332,6 +332,9 @@ "average-stress-level": "Average Stress Level", "awake-time": "Awake Time", "back": "Back", + "blood-glucose": "Blood Glucose", + "blood-glucose-max": "Max Blood Glucose", + "blood-glucose-min": "Min Blood Glucose", "blood-pressure-reading-classification-elevated": "Elevated", "blood-pressure-reading-classification-hypertension-stage-1": "Hypertension Stage 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hypertension Stage 2", diff --git a/locales/es.json b/locales/es.json index 501f0f278..ed90e74ce 100644 --- a/locales/es.json +++ b/locales/es.json @@ -332,6 +332,9 @@ "average-stress-level": "Nivel Promedio de Estrés", "awake-time": "Tiempo Despierto", "back": "Atrás", + "blood-glucose": "Glucosa en sangre", + "blood-glucose-max": "Glucosa máxima en sangre", + "blood-glucose-min": "Glucosa mínima en sangre", "blood-pressure-reading-classification-elevated": "Elevada", "blood-pressure-reading-classification-hypertension-stage-1": "Hipertensión Etapa 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hipertensión Etapa 2", diff --git a/locales/fil.json b/locales/fil.json index d97a0c4b5..5da938eb6 100644 --- a/locales/fil.json +++ b/locales/fil.json @@ -332,6 +332,9 @@ "average-stress-level": "Average na Antas ng Stress", "awake-time": "Oras ng Paggising", "back": "Bumalik", + "blood-glucose": "Blood Glucose", + "blood-glucose-max": "Max. Blood Glucose", + "blood-glucose-min": "Min. Blood Glucose", "blood-pressure-reading-classification-elevated": "Mataas", "blood-pressure-reading-classification-hypertension-stage-1": "Altapresyon Yugto 1", "blood-pressure-reading-classification-hypertension-stage-2": "Altapresyon Yugto 2", diff --git a/locales/fr-CA.json b/locales/fr-CA.json index 1038ea587..33d175574 100644 --- a/locales/fr-CA.json +++ b/locales/fr-CA.json @@ -332,6 +332,9 @@ "average-stress-level": "Niveau de stress moyen", "awake-time": "Temps éveillé", "back": "Retour", + "blood-glucose": "Glycémie", + "blood-glucose-max": "Glycémie maximale", + "blood-glucose-min": "Glycémie minimale", "blood-pressure-reading-classification-elevated": "Élevée", "blood-pressure-reading-classification-hypertension-stage-1": "Hypertension Stade 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hypertension Stade 2", diff --git a/locales/fr.json b/locales/fr.json index 12de38cb1..3fc555804 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -332,6 +332,9 @@ "average-stress-level": "Niveau de Stress Moyen", "awake-time": "Temps Éveillé", "back": "Retour", + "blood-glucose": "Glycémie", + "blood-glucose-max": "Glycémie maximale", + "blood-glucose-min": "Glycémie minimale", "blood-pressure-reading-classification-elevated": "Élevée", "blood-pressure-reading-classification-hypertension-stage-1": "Hypertension Stade 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hypertension Stade 2", diff --git a/locales/it.json b/locales/it.json index 2784da7d3..b5461f432 100644 --- a/locales/it.json +++ b/locales/it.json @@ -332,6 +332,9 @@ "average-stress-level": "Livello Medio di Stress", "awake-time": "Tempo Sveglio", "back": "Indietro", + "blood-glucose": "Glicemia", + "blood-glucose-max": "Glicemia massima", + "blood-glucose-min": "Glicemia minima", "blood-pressure-reading-classification-elevated": "Elevata", "blood-pressure-reading-classification-hypertension-stage-1": "Ipertensione Stadio 1", "blood-pressure-reading-classification-hypertension-stage-2": "Ipertensione Stadio 2", diff --git a/locales/nl.json b/locales/nl.json index 9d501e69d..a09c3307e 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -332,6 +332,9 @@ "average-stress-level": "Gemiddeld Stressniveau", "awake-time": "Wakkere Tijd", "back": "Terug", + "blood-glucose": "Bloedglucose", + "blood-glucose-max": "Maximale bloedglucose", + "blood-glucose-min": "Minimale bloedglucose", "blood-pressure-reading-classification-elevated": "Verhoogd", "blood-pressure-reading-classification-hypertension-stage-1": "Hypertensie Fase 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hypertensie Fase 2", diff --git a/locales/pl.json b/locales/pl.json index 76fa36632..05ddcb6d5 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -332,6 +332,9 @@ "average-stress-level": "Średni poziom stresu", "awake-time": "Czas czuwania", "back": "Wstecz", + "blood-glucose": "Poziom glukozy", + "blood-glucose-max": "Maksymalny poziom glukozy we krwi", + "blood-glucose-min": "Min. poziom glukozy we krwi", "blood-pressure-reading-classification-elevated": "Podwyższone", "blood-pressure-reading-classification-hypertension-stage-1": "Nadciśnienie Etap 1", "blood-pressure-reading-classification-hypertension-stage-2": "Nadciśnienie Etap 2", diff --git a/locales/pt-PT.json b/locales/pt-PT.json index e93e406fd..c59a3258d 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -332,6 +332,9 @@ "average-stress-level": "Nível médio de stress", "awake-time": "Tempo acordado", "back": "Voltar", + "blood-glucose": "Glicose no sangue", + "blood-glucose-max": "Glicemia máxima", + "blood-glucose-min": "Glicemia mínima", "blood-pressure-reading-classification-elevated": "Elevada", "blood-pressure-reading-classification-hypertension-stage-1": "Hipertensão Estádio 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hipertensão Estádio 2", diff --git a/locales/pt.json b/locales/pt.json index 5bc1cf7de..a9b2d265e 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -332,6 +332,9 @@ "average-stress-level": "Nível Médio de Estresse", "awake-time": "Tempo Acordado", "back": "Voltar", + "blood-glucose": "Glicose no sangue", + "blood-glucose-max": "Glicemia máxima", + "blood-glucose-min": "Glicemia mínima", "blood-pressure-reading-classification-elevated": "Elevada", "blood-pressure-reading-classification-hypertension-stage-1": "Hipertensão Estágio 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hipertensão Estágio 2", diff --git a/locales/ro.json b/locales/ro.json index 7c27ed810..5ee6dbe72 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -332,6 +332,9 @@ "average-stress-level": "Nivel mediu de stres", "awake-time": "Timp treaz", "back": "Înapoi", + "blood-glucose": "Glicemie", + "blood-glucose-max": "Glicemie maximă", + "blood-glucose-min": "Glicemie minimă", "blood-pressure-reading-classification-elevated": "Ridicată", "blood-pressure-reading-classification-hypertension-stage-1": "Hipertensiune Stadiul 1", "blood-pressure-reading-classification-hypertension-stage-2": "Hipertensiune Stadiul 2", diff --git a/locales/so.json b/locales/so.json index f19234351..fef15ead3 100644 --- a/locales/so.json +++ b/locales/so.json @@ -332,6 +332,9 @@ "average-stress-level": "Celceliska Heerka Walaaca", "awake-time": "Waqtiga Soo Jeedka", "back": "Dib", + "blood-glucose": "Sonkorowga Dhiigga", + "blood-glucose-max": "Gulukoosta Dhiigga ugu Badan", + "blood-glucose-min": "Gulukoosta Dhiigga ee Ugu Yar", "blood-pressure-reading-classification-elevated": "Sare", "blood-pressure-reading-classification-hypertension-stage-1": "Cadaadis Dhiig Marxalad 1", "blood-pressure-reading-classification-hypertension-stage-2": "Cadaadis Dhiig Marxalad 2", diff --git a/locales/sw.json b/locales/sw.json index c6518e221..8ca2da105 100644 --- a/locales/sw.json +++ b/locales/sw.json @@ -332,6 +332,9 @@ "average-stress-level": "Wastani wa Kiwango cha Msongo", "awake-time": "Muda wa Kuamka", "back": "Rudi", + "blood-glucose": "Sukari katika Damu", + "blood-glucose-max": "Glukosi ya Damu ya Juu Zaidi", + "blood-glucose-min": "Glukosi ya Damu ya Chini", "blood-pressure-reading-classification-elevated": "Imeinuliwa", "blood-pressure-reading-classification-hypertension-stage-1": "Shinikizo la Damu Hatua ya 1", "blood-pressure-reading-classification-hypertension-stage-2": "Shinikizo la Damu Hatua ya 2", diff --git a/locales/tl.json b/locales/tl.json index 168b853f3..8e69a1aa7 100644 --- a/locales/tl.json +++ b/locales/tl.json @@ -332,6 +332,9 @@ "average-stress-level": "Average na Antas ng Stress", "awake-time": "Oras ng Paggising", "back": "Bumalik", + "blood-glucose": "Blood Glucose", + "blood-glucose-max": "Max. Blood Glucose", + "blood-glucose-min": "Min. Blood Glucose", "blood-pressure-reading-classification-elevated": "Mataas", "blood-pressure-reading-classification-hypertension-stage-1": "Altapresyon Yugto 1", "blood-pressure-reading-classification-hypertension-stage-2": "Altapresyon Yugto 2", diff --git a/locales/vi.json b/locales/vi.json index 6cef2a9c0..7b13d385a 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -332,6 +332,9 @@ "average-stress-level": "Mức độ căng thẳng trung bình", "awake-time": "Thời gian thức", "back": "Quay lại", + "blood-glucose": "Đường huyết", + "blood-glucose-max": "Lượng đường huyết tối đa", + "blood-glucose-min": "Lượng đường huyết tối thiểu", "blood-pressure-reading-classification-elevated": "Cao", "blood-pressure-reading-classification-hypertension-stage-1": "Tăng huyết áp Giai đoạn 1", "blood-pressure-reading-classification-hypertension-stage-2": "Tăng huyết áp Giai đoạn 2", diff --git a/src/helpers/daily-data-providers/apple-health-blood-glucose.ts b/src/helpers/daily-data-providers/apple-health-blood-glucose.ts new file mode 100644 index 000000000..b0cb5ea16 --- /dev/null +++ b/src/helpers/daily-data-providers/apple-health-blood-glucose.ts @@ -0,0 +1,6 @@ +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; + +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('AppleHealth', 'Blood Glucose', startDate, endDate, 'avg'); +} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/apple-health-heart-rate-range.ts b/src/helpers/daily-data-providers/apple-health-heart-rate-range.ts index 7b240dc40..e4ee14c2f 100644 --- a/src/helpers/daily-data-providers/apple-health-heart-rate-range.ts +++ b/src/helpers/daily-data-providers/apple-health-heart-rate-range.ts @@ -1,20 +1,15 @@ import { DailyDataQueryResult } from "../query-daily-data"; -import { getFloatValue, getStartDate, queryForDailyData } from "./daily-data"; -import { DeviceDataPoint } from "@careevolution/mydatahelps-js"; +import { appleHealthMaxHeartRateDataProvider, appleHealthMinHeartRateDataProvider } from './index'; -export default async function (startDate: Date, endDate: Date): Promise { - const dailyData = await queryForDailyData("AppleHealth", ["HourlyMaximumHeartRate", "HourlyMinimumHeartRate"], startDate, endDate, getStartDate); +export default async function(startDate: Date, endDate: Date): Promise { + const [minResult, maxResult] = await Promise.all([ + appleHealthMinHeartRateDataProvider(startDate, endDate), + appleHealthMaxHeartRateDataProvider(startDate, endDate) + ]); - const getHeartRate = (dataPoints: DeviceDataPoint[], type: string, aggregateFn: typeof Math.min | typeof Math.max): number => { - const typeFilteredDataPoints = dataPoints.filter(dataPoint => dataPoint.type === type); - return typeFilteredDataPoints.length > 0 ? aggregateFn(...typeFilteredDataPoints.map(getFloatValue)) : 0; - }; - - return Object.entries(dailyData).reduce((result, [dayKey, dataPoints]) => { - const maxHeartRate = getHeartRate(dataPoints, "HourlyMaximumHeartRate", Math.max); - const minHeartRate = getHeartRate(dataPoints, "HourlyMinimumHeartRate", Math.min); - if (maxHeartRate && minHeartRate) { - result[dayKey] = maxHeartRate - minHeartRate; + return Object.entries(minResult).reduce((result, [dayKey, minHeartRate]) => { + if (maxResult[dayKey] > minHeartRate) { + result[dayKey] = maxResult[dayKey] - minHeartRate; } return result; }, {} as DailyDataQueryResult); diff --git a/src/helpers/daily-data-providers/apple-health-max-blood-glucose.ts b/src/helpers/daily-data-providers/apple-health-max-blood-glucose.ts new file mode 100644 index 000000000..e85f52c01 --- /dev/null +++ b/src/helpers/daily-data-providers/apple-health-max-blood-glucose.ts @@ -0,0 +1,6 @@ +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; + +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('AppleHealth', 'Blood Glucose', startDate, endDate, 'max'); +} \ No newline at end of file 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 31d0f02f1..5e79bfeba 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,7 +1,6 @@ -import { DailyDataQueryResult } from "../query-daily-data"; -import { buildMaxValueResult, getStartDate, queryForDailyData } from "./daily-data"; +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; -export default async function (startDate: Date, endDate: Date): Promise { - const dailyData = await queryForDailyData("AppleHealth", "HourlyMaximumHeartRate", startDate, endDate, getStartDate); - return buildMaxValueResult(dailyData); +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('AppleHealth', 'Hourly Maximum Heart Rate', startDate, endDate, 'max'); } diff --git a/src/helpers/daily-data-providers/apple-health-min-blood-glucose.ts b/src/helpers/daily-data-providers/apple-health-min-blood-glucose.ts new file mode 100644 index 000000000..41d4e5614 --- /dev/null +++ b/src/helpers/daily-data-providers/apple-health-min-blood-glucose.ts @@ -0,0 +1,6 @@ +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; + +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('AppleHealth', 'Blood Glucose', startDate, endDate, 'min'); +} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/apple-health-min-heart-rate.ts b/src/helpers/daily-data-providers/apple-health-min-heart-rate.ts new file mode 100644 index 000000000..e885930de --- /dev/null +++ b/src/helpers/daily-data-providers/apple-health-min-heart-rate.ts @@ -0,0 +1,6 @@ +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; + +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('AppleHealth', 'Hourly Minimum Heart Rate', startDate, endDate, 'min'); +} diff --git a/src/helpers/daily-data-providers/apple-health-mindful-minutes.ts b/src/helpers/daily-data-providers/apple-health-mindful-minutes.ts index 037e7bda3..d16051b2e 100644 --- a/src/helpers/daily-data-providers/apple-health-mindful-minutes.ts +++ b/src/helpers/daily-data-providers/apple-health-mindful-minutes.ts @@ -1,10 +1,10 @@ import { DailyDataQueryResult } from '../query-daily-data'; import { isSilverCloudCbtDataPoint } from './common-mindful-and-therapy'; import { buildMinutesResultFromDailyTimeRanges, computeDailyTimeRanges } from '../time-range'; -import { queryForDailyDataPoints } from './daily-data'; +import { queryForDailyDataPointsV2 } from './daily-data'; -export default async function (startDate: Date, endDate: Date): Promise { - const dataPoints = await queryForDailyDataPoints('AppleHealth', 'MindfulSession', startDate, endDate); +export default async function(startDate: Date, endDate: Date): Promise { + const dataPoints = await queryForDailyDataPointsV2('AppleHealth', 'Mindful Sessions', startDate, endDate); const dailyTimeRanges = computeDailyTimeRanges(dataPoints.filter(dataPoint => !isSilverCloudCbtDataPoint(dataPoint))); return buildMinutesResultFromDailyTimeRanges(startDate, endDate, dailyTimeRanges); } \ No newline at end of file diff --git a/src/helpers/daily-data-providers/apple-health-resting-heart-rate.ts b/src/helpers/daily-data-providers/apple-health-resting-heart-rate.ts index e8d0412c9..ca41eb2f7 100644 --- a/src/helpers/daily-data-providers/apple-health-resting-heart-rate.ts +++ b/src/helpers/daily-data-providers/apple-health-resting-heart-rate.ts @@ -1,7 +1,6 @@ -import { DailyDataQueryResult } from "../query-daily-data"; -import { buildAverageValueResult, getStartDate, queryForDailyData } from "./daily-data"; +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; -export default async function (startDate: Date, endDate: Date): Promise { - const dailyData = await queryForDailyData("AppleHealth", "RestingHeartRate", startDate, endDate, getStartDate); - return buildAverageValueResult(dailyData); +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('AppleHealth', 'Resting Heart Rate', startDate, endDate, 'avg'); } \ No newline at end of file diff --git a/src/helpers/daily-data-providers/apple-health-sleep-v2.ts b/src/helpers/daily-data-providers/apple-health-sleep-v2.ts deleted file mode 100644 index 9c49cbc75..000000000 --- a/src/helpers/daily-data-providers/apple-health-sleep-v2.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { querySleepMinutesV2 } from './common-sleep-v2'; -import { DailyDataQueryResult } from '../query-daily-data'; - -export function totalSleepMinutes(startDate: Date, endDate: Date): Promise { - return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['AsleepCore', 'AsleepREM', 'AsleepDeep', 'Asleep']); -} - -export function coreSleepMinutes(startDate: Date, endDate: Date): Promise { - return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['AsleepCore']); -} - -export function remSleepMinutes(startDate: Date, endDate: Date): Promise { - return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['AsleepREM']); -} - -export function deepSleepMinutes(startDate: Date, endDate: Date): Promise { - return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['AsleepDeep']); -} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/apple-health-sleep.ts b/src/helpers/daily-data-providers/apple-health-sleep.ts index 5af9d610c..e4a9b5c86 100644 --- a/src/helpers/daily-data-providers/apple-health-sleep.ts +++ b/src/helpers/daily-data-providers/apple-health-sleep.ts @@ -1,31 +1,22 @@ -import { buildMinutesResultFromDailyTimeRanges, computeDailyTimeRanges } from "../time-range"; -import { DailyDataQueryResult } from "../query-daily-data"; -import { queryForDailyDataPoints } from "./daily-data"; - -type SleepType = "Asleep" | "InBed" | "AsleepCore" | "AsleepREM" | "AsleepDeep"; - -async function coreSleep(startDate: Date, endDate: Date, values: SleepType[]): Promise { - const dataPoints = await queryForDailyDataPoints("AppleHealth", "SleepAnalysisInterval", startDate, endDate); - const dailyTimeRanges = computeDailyTimeRanges(dataPoints.filter(dataPoint => values.includes(dataPoint.value as SleepType)), -6); - return buildMinutesResultFromDailyTimeRanges(startDate, endDate, dailyTimeRanges); -} +import { DailyDataQueryResult } from '../query-daily-data'; +import { querySleepMinutesV2 } from './common-sleep-v2'; export function inBedTime(startDate: Date, endDate: Date): Promise { - return coreSleep(startDate, endDate, ["InBed"]); + return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['InBed']); } export function asleepCoreTime(startDate: Date, endDate: Date): Promise { - return coreSleep(startDate, endDate, ["AsleepCore"]); + return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['AsleepCore']); } export function asleepRemTime(startDate: Date, endDate: Date): Promise { - return coreSleep(startDate, endDate, ["AsleepREM"]); + return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['AsleepREM']); } export function asleepDeepTime(startDate: Date, endDate: Date): Promise { - return coreSleep(startDate, endDate, ["AsleepDeep"]); + return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['AsleepDeep']); } export function asleepTime(startDate: Date, endDate: Date): Promise { - return coreSleep(startDate, endDate, ["AsleepCore", "AsleepREM", "AsleepDeep", "Asleep"]); + return querySleepMinutesV2('AppleHealth', 'Sleep Analysis', startDate, endDate, ['AsleepCore', 'AsleepREM', 'AsleepDeep', 'Asleep']); } \ No newline at end of file diff --git a/src/helpers/daily-data-providers/apple-health-steps-while-wearing-device.ts b/src/helpers/daily-data-providers/apple-health-steps-while-wearing-device.ts index a49aed035..a739b0363 100644 --- a/src/helpers/daily-data-providers/apple-health-steps-while-wearing-device.ts +++ b/src/helpers/daily-data-providers/apple-health-steps-while-wearing-device.ts @@ -3,9 +3,9 @@ import getDayKey from '../get-day-key'; import { DailyDataQueryResult } from '../query-daily-data'; import queryAllDeviceDataV2Aggregates from '../query-all-device-data-v2-aggregates'; import { DeviceDataV2Aggregate } from '@careevolution/mydatahelps-js'; -import { buildTotalValueResult, DailyData, getStartDate, queryForDailyData } from "./daily-data"; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; -export default async function (startDate: Date, endDate: Date): Promise { +export default async function(startDate: Date, endDate: Date): Promise { const convertAggregatesToDates = (aggregates: DeviceDataV2Aggregate[]): string[] => { return aggregates.reduce((dates, aggregate) => { const dayKey = getDayKey(aggregate.date); @@ -38,12 +38,9 @@ export default async function (startDate: Date, endDate: Date): Promise watchDates.includes(dayKey) || ouraDates.includes(dayKey)) - ) as DailyData; - - return buildTotalValueResult(filteredDailyData); + const result = await queryAggregateDailyData('AppleHealth', 'Hourly Steps', startDate, endDate, 'sum'); + return Object.fromEntries( + Object.entries(result).filter(([dayKey]) => watchDates.includes(dayKey) || ouraDates.includes(dayKey)) + ) as DailyDataQueryResult; } diff --git a/src/helpers/daily-data-providers/apple-health-steps.ts b/src/helpers/daily-data-providers/apple-health-steps.ts index b2899785c..5d77407ef 100644 --- a/src/helpers/daily-data-providers/apple-health-steps.ts +++ b/src/helpers/daily-data-providers/apple-health-steps.ts @@ -1,7 +1,6 @@ -import { DailyDataQueryResult } from "../query-daily-data"; -import { buildTotalValueResult, getStartDate, queryForDailyData } from "./daily-data"; +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; -export default async function (startDate: Date, endDate: Date): Promise { - const dailyData = await queryForDailyData("AppleHealth", "HourlySteps", startDate, endDate, getStartDate); - return buildTotalValueResult(dailyData); +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('AppleHealth', 'Hourly Steps', startDate, endDate, 'sum'); } diff --git a/src/helpers/daily-data-providers/combined-blood-glucose.ts b/src/helpers/daily-data-providers/combined-blood-glucose.ts new file mode 100644 index 000000000..872fc35bc --- /dev/null +++ b/src/helpers/daily-data-providers/combined-blood-glucose.ts @@ -0,0 +1,22 @@ +import { appleHealthBloodGlucoseDataProvider, healthConnectBloodGlucoseDataProvider } from '.'; +import { DailyDataQueryResult } from '../query-daily-data'; +import { getCombinedDataCollectionSettings } from './combined-data-collection-settings'; +import { combineResultsUsingFirstValue } from './daily-data'; + +export default async function(startDate: Date, endDate: Date): Promise { + const providers: Promise[] = []; + + const { settings, deviceDataV2Types } = await getCombinedDataCollectionSettings(true); + + if (settings.appleHealthEnabled && deviceDataV2Types.some(type => type.namespace === 'AppleHealth' && type.type === 'Blood Glucose')) { + providers.push(appleHealthBloodGlucoseDataProvider(startDate, endDate)); + } + if (settings.healthConnectEnabled && deviceDataV2Types.some(type => type.namespace === 'HealthConnect' && type.type === 'blood-glucose')) { + providers.push(healthConnectBloodGlucoseDataProvider(startDate, endDate)); + } + + if (providers.length === 0) return {}; + if (providers.length === 1) return providers[0]; + + return combineResultsUsingFirstValue(startDate, endDate, await Promise.all(providers)); +} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/combined-mindful-minutes.ts b/src/helpers/daily-data-providers/combined-mindful-minutes.ts index e9f43f33f..ec01a32d1 100644 --- a/src/helpers/daily-data-providers/combined-mindful-minutes.ts +++ b/src/helpers/daily-data-providers/combined-mindful-minutes.ts @@ -8,7 +8,7 @@ export default async function (startDate: Date, endDate: Date): Promise type.namespace === 'AppleHealth' && type.type === 'MindfulSession')) { + if (settings.appleHealthEnabled && deviceDataV2Types.some(type => type.namespace === 'AppleHealth' && type.type === 'Mindful Sessions')) { providers.push(appleHealthMindfulMinutesDataProvider(startDate, endDate)); } if (settings.googleFitEnabled && settings.queryableDeviceDataTypes.some(type => type.namespace === 'GoogleFit' && type.type === 'ActivitySegment')) { diff --git a/src/helpers/daily-data-providers/combined-resting-heart-rate.ts b/src/helpers/daily-data-providers/combined-resting-heart-rate.ts index 68c5e53db..3d82983a6 100644 --- a/src/helpers/daily-data-providers/combined-resting-heart-rate.ts +++ b/src/helpers/daily-data-providers/combined-resting-heart-rate.ts @@ -14,7 +14,7 @@ export default async function (startDate: Date, endDate: Date): Promise ddt.namespace === "AppleHealth" && ddt.type === "RestingHeartRate")) { + if (settings.appleHealthEnabled && deviceDataV2Types.some(ddt => ddt.namespace === "AppleHealth" && ddt.type === "Resting Heart Rate")) { providers.push(appleHealthRestingHeartRateDataProvider(startDate, endDate)); } if (settings.healthConnectEnabled && deviceDataV2Types.some(ddt => ddt.namespace === "HealthConnect" && ddt.type === "resting-heart-rate")) { diff --git a/src/helpers/daily-data-providers/combined-sleep.ts b/src/helpers/daily-data-providers/combined-sleep.ts index 2da03022b..63357efd2 100644 --- a/src/helpers/daily-data-providers/combined-sleep.ts +++ b/src/helpers/daily-data-providers/combined-sleep.ts @@ -14,7 +14,7 @@ export default async function (startDate: Date, endDate: Date, combinedDataColle if (settings.garminEnabled) { providers.push(garminTotalSleepMinutesDataProvider(startDate, endDate)); } - if (settings.appleHealthEnabled && settings.queryableDeviceDataTypes.some(ddt => ddt.namespace === "AppleHealth" && ddt.type === "SleepAnalysisInterval")) { + if (settings.appleHealthEnabled && deviceDataV2Types.some(ddt => ddt.namespace === "AppleHealth" && ddt.type === "Sleep Analysis")) { providers.push(appleHealthSleepDataProvider(startDate, endDate)); } if (settings.healthConnectEnabled && deviceDataV2Types.some(ddt => ddt.namespace === "HealthConnect" && ddt.type === "sleep")) { diff --git a/src/helpers/daily-data-providers/combined-steps.ts b/src/helpers/daily-data-providers/combined-steps.ts index fe77f37af..6c1c2bad0 100644 --- a/src/helpers/daily-data-providers/combined-steps.ts +++ b/src/helpers/daily-data-providers/combined-steps.ts @@ -14,7 +14,7 @@ export default async function (startDate: Date, endDate: Date, includeGoogleFit? if (settings.garminEnabled) { providers.push(garminStepsDataProvider(startDate, endDate)); } - if (settings.appleHealthEnabled && settings.queryableDeviceDataTypes.some(ddt => ddt.namespace === "AppleHealth" && ddt.type === "HourlySteps")) { + if (settings.appleHealthEnabled && deviceDataV2Types.some(ddt => ddt.namespace === "AppleHealth" && ddt.type === "Hourly Steps")) { providers.push(appleHealthStepsDataProvider(startDate, endDate)); } if (settings.healthConnectEnabled && deviceDataV2Types.some(ddt => ddt.namespace === "HealthConnect" && ddt.type === "steps-daily")) { diff --git a/src/helpers/daily-data-providers/common-mindful-and-therapy.ts b/src/helpers/daily-data-providers/common-mindful-and-therapy.ts index 92868affd..d0f66c0c1 100644 --- a/src/helpers/daily-data-providers/common-mindful-and-therapy.ts +++ b/src/helpers/daily-data-providers/common-mindful-and-therapy.ts @@ -1,6 +1,15 @@ -import { DeviceDataPoint } from '@careevolution/mydatahelps-js'; +import { DeviceDataPoint, DeviceDataV2Point } from '@careevolution/mydatahelps-js'; -export function isSilverCloudCbtDataPoint(dataPoint: DeviceDataPoint): boolean { - return dataPoint.source?.properties?.['SourceIdentifier'] === 'com.silvercloudhealth.SilverCloud' - && dataPoint.properties?.['Metadata_sub-type'] === 'CBT'; +export function isSilverCloudCbtDataPoint(dataPoint: DeviceDataPoint | DeviceDataV2Point): boolean { + if (dataPoint.properties?.['Metadata_sub-type'] !== 'CBT') return false; + + if ("dataSource" in dataPoint) { + return dataPoint.dataSource?.['SourceIdentifier'] === 'com.silvercloudhealth.SilverCloud'; + } + + if ("source" in dataPoint) { + return dataPoint.source?.properties?.['SourceIdentifier'] === 'com.silvercloudhealth.SilverCloud'; + } + + return false; } \ No newline at end of file diff --git a/src/helpers/daily-data-providers/daily-data/daily-data-aggregate.ts b/src/helpers/daily-data-providers/daily-data/daily-data-aggregate.ts new file mode 100644 index 000000000..4f0da5aa0 --- /dev/null +++ b/src/helpers/daily-data-providers/daily-data/daily-data-aggregate.ts @@ -0,0 +1,26 @@ +import queryAllDeviceDataV2Aggregates from '../../query-all-device-data-v2-aggregates'; +import { add } from 'date-fns'; +import getDayKey from '../../get-day-key'; +import { DailyDataQueryResult } from '../../query-daily-data'; +import { DeviceDataV2Namespace } from '@careevolution/mydatahelps-js'; + +export type AggregateFunction = 'sum' | 'avg' | 'min' | 'max' | 'count'; + +export async function queryAggregateDailyData(namespace: DeviceDataV2Namespace, type: string, startDate: Date, endDate: Date, aggregateFn: AggregateFunction, scaleFactor?: number): Promise { + const aggregates = await queryAllDeviceDataV2Aggregates({ + namespace: namespace, + type: type, + observedAfter: add(startDate, { days: -1 }).toISOString(), + observedBefore: add(endDate, { days: 1 }).toISOString(), + intervalAmount: 1, + intervalType: 'Days', + aggregateFunctions: [aggregateFn] + }); + return aggregates.reduce((result, aggregate) => { + const aggregateValue = aggregate.statistics[aggregateFn] * (scaleFactor ?? 1); + if (aggregateValue > 0) { + result[getDayKey(aggregate.date)] = aggregateValue; + } + return result; + }, {} as DailyDataQueryResult); +} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/daily-data/daily-data-result.ts b/src/helpers/daily-data-providers/daily-data/daily-data-result.ts index fc9b2d4b3..4d7de09e0 100644 --- a/src/helpers/daily-data-providers/daily-data/daily-data-result.ts +++ b/src/helpers/daily-data-providers/daily-data/daily-data-result.ts @@ -16,14 +16,26 @@ export const buildMostRecentValueResult = (dailyData: DailyData | DailyDataV2, v return result; }; +export const buildMinValueResult = (dailyData: DailyData | DailyDataV2, valueFn: DailyDataValueFunction = getFloatValue): DailyDataQueryResult => { + const result: DailyDataQueryResult = {}; + + Object.keys(dailyData).forEach(dayKey => { + const dayValues = dailyData[dayKey].map(valueFn).filter(value => value > 0); + if (dayValues.length > 0) { + result[dayKey] = Math.min(...dayValues); + } + }); + + return result; +}; + export const buildMaxValueResult = (dailyData: DailyData | DailyDataV2, valueFn: DailyDataValueFunction = getFloatValue): DailyDataQueryResult => { const result: DailyDataQueryResult = {}; Object.keys(dailyData).forEach(dayKey => { - const dayValues = dailyData[dayKey].map(valueFn); - const maxValue = Math.max(...dayValues); - if (maxValue > 0) { - result[dayKey] = maxValue; + const dayValues = dailyData[dayKey].map(valueFn).filter(value => value > 0); + if (dayValues.length > 0) { + result[dayKey] = Math.max(...dayValues); } }); @@ -42,7 +54,7 @@ export const buildTotalValueResult = (dailyData: DailyData | DailyDataV2, valueF }); return result; -} +}; export const buildAverageValueResult = (dailyData: DailyData | DailyDataV2, valueFn: DailyDataValueFunction = getFloatValue): DailyDataQueryResult => { const result: DailyDataQueryResult = {}; @@ -51,12 +63,12 @@ export const buildAverageValueResult = (dailyData: DailyData | DailyDataV2, valu const dayValues = dailyData[dayKey].map(valueFn).filter(value => value > 0); const totalValue = dayValues.reduce((a, b) => a + b, 0); if (totalValue > 0) { - result[dayKey] = totalValue / dayValues.length + result[dayKey] = totalValue / dayValues.length; } }); return result; -} +}; export function combineResultsUsingFirstValue(startDate: Date, endDate: Date, resultsToCombine: DailyDataQueryResult[]): DailyDataQueryResult { return combineResults(startDate, endDate, resultsToCombine, values => values[0]); diff --git a/src/helpers/daily-data-providers/health-connect-blood-glucose.ts b/src/helpers/daily-data-providers/health-connect-blood-glucose.ts new file mode 100644 index 000000000..a85d1f581 --- /dev/null +++ b/src/helpers/daily-data-providers/health-connect-blood-glucose.ts @@ -0,0 +1,8 @@ +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; + +const MMOL_TO_MGDL_SCALE_FACTOR = 18; + +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('HealthConnect', 'blood-glucose', startDate, endDate, 'avg', MMOL_TO_MGDL_SCALE_FACTOR); +} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/health-connect-max-blood-glucose.ts b/src/helpers/daily-data-providers/health-connect-max-blood-glucose.ts new file mode 100644 index 000000000..fedaab282 --- /dev/null +++ b/src/helpers/daily-data-providers/health-connect-max-blood-glucose.ts @@ -0,0 +1,8 @@ +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; + +const MMOL_TO_MGDL_SCALE_FACTOR = 18; + +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('HealthConnect', 'blood-glucose', startDate, endDate, 'max', MMOL_TO_MGDL_SCALE_FACTOR); +} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/health-connect-min-blood-glucose.ts b/src/helpers/daily-data-providers/health-connect-min-blood-glucose.ts new file mode 100644 index 000000000..cc391af32 --- /dev/null +++ b/src/helpers/daily-data-providers/health-connect-min-blood-glucose.ts @@ -0,0 +1,8 @@ +import { DailyDataQueryResult } from '../query-daily-data'; +import { queryAggregateDailyData } from './daily-data/daily-data-aggregate'; + +const MMOL_TO_MGDL_SCALE_FACTOR = 18; + +export default async function(startDate: Date, endDate: Date): Promise { + return queryAggregateDailyData('HealthConnect', 'blood-glucose', startDate, endDate, 'min', MMOL_TO_MGDL_SCALE_FACTOR); +} \ No newline at end of file diff --git a/src/helpers/daily-data-providers/index.ts b/src/helpers/daily-data-providers/index.ts index 658ca9e42..e46ab9fd3 100644 --- a/src/helpers/daily-data-providers/index.ts +++ b/src/helpers/daily-data-providers/index.ts @@ -3,6 +3,7 @@ export { default as appleHealthFlightsClimbedDataProvider } from "./apple-health 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 { default as appleHealthMinHeartRateDataProvider } from "./apple-health-min-heart-rate" export { default as appleHealthMindfulMinutesDataProvider } from "./apple-health-mindful-minutes" export { default as appleHealthRestingHeartRateDataProvider } from "./apple-health-resting-heart-rate" export { asleepTime as appleHealthSleepDataProvider } from "./apple-health-sleep" @@ -17,6 +18,9 @@ export { default as appleHealthTherapyMinutesDataProvider } from "./apple-health export { default as appleHealthWalkingHeartRateAverageDataProvider } from "./apple-health-walking-heart-rate-average" export { default as appleHealthActiveEnergyBurnedDataProvider } from "./apple-health-active-energy-burned" export { default as appleHealthNumberOfAlcoholicBeveragesDataProvider } from "./apple-health-number-of-alcoholic-beverages" +export { default as appleHealthBloodGlucoseDataProvider } from "./apple-health-blood-glucose" +export { default as appleHealthMinBloodGlucoseDataProvider } from "./apple-health-min-blood-glucose" +export { default as appleHealthMaxBloodGlucoseDataProvider } from "./apple-health-max-blood-glucose" export { sedentaryMinutes as fitbitSedentaryMinutesDataProvider } from "./fitbit-activity-minutes" export { totalActiveMinutes as fitbitTotalActiveMinutesDataProvider } from "./fitbit-activity-minutes" export { lightlyActiveMinutes as fitbitLightlyActiveMinutesDataProvider } from "./fitbit-activity-minutes" @@ -46,6 +50,7 @@ export { default as combinedStepsDataProvider } from "./combined-steps" export { default as combinedActiveCaloriesBurnedDataProvider } from "./combined-active-calories-burned" export { default as combinedSleepDataProvider } from "./combined-sleep" export { default as combinedTherapyMinutesDataProvider } from "./combined-therapy-minutes" +export { default as combinedBloodGlucoseDataProvider } from "./combined-blood-glucose" export { default as googleFitMindfulMinutesDataProvider } from "./google-fit-mindful-minutes" export { default as googleFitStepsDataProvider } from "./google-fit-steps" export { default as googleFitTherapyMinutesDataProvider } from "./google-fit-therapy-minutes" @@ -89,6 +94,9 @@ export { default as healthConnectActiveCaloriesBurnedDataProvider } from "./heal export { default as healthConnectTotalCaloriesBurnedDataProvider } from "./health-connect-total-calories-burned"; export { default as healthConnectTherapyMinutesDataProvider } from "./health-connect-therapy-minutes"; export { default as healthConnectMindfulMinutesDataProvider } from "./health-connect-mindful-minutes"; +export { default as healthConnectBloodGlucoseDataProvider } from "./health-connect-blood-glucose"; +export { default as healthConnectMinBloodGlucoseDataProvider } from "./health-connect-min-blood-glucose"; +export { default as healthConnectMaxBloodGlucoseDataProvider } from "./health-connect-max-blood-glucose"; export { default as ouraStepsDataProvider } from "./oura-daily-steps" export { default as ouraSleepMinutesDataProvider } from "./oura-total-sleep" export { default as ouraRestingHeartRateDataProvider } from "./oura-resting-heart-rate" diff --git a/src/helpers/daily-data-types.tsx b/src/helpers/daily-data-types.tsx index 952264f23..204617987 100644 --- a/src/helpers/daily-data-types.tsx +++ b/src/helpers/daily-data-types.tsx @@ -8,6 +8,7 @@ export enum DailyDataType { AppleHealthHeartRateRange = "AppleHealthHeartRateRange", AppleHealthHrv = "AppleHealthHrv", AppleHealthMaxHeartRate = "AppleHealthMaxHeartRate", + AppleHealthMinHeartRate = "AppleHealthMinHeartRate", AppleHealthMindfulMinutes = "AppleHealthMindfulMinutes", AppleHealthRestingHeartRate = "AppleHealthRestingHeartRate", AppleHealthSleepMinutes = "AppleHealthSleepMinutes", @@ -22,6 +23,9 @@ export enum DailyDataType { AppleHealthWalkingHeartRateAverage = "AppleHealthWalkingHeartRateAverage", AppleHealthActiveEnergyBurned = "AppleHealthActiveEnergyBurned", AppleHealthNumberOfAlcoholicBeverages = "AppleHealthNumberOfAlcoholicBeverages", + AppleHealthBloodGlucose = "AppleHealthBloodGlucose", + AppleHealthMinBloodGlucose = "AppleHealthMinBloodGlucose", + AppleHealthMaxBloodGlucose = "AppleHealthMaxBloodGlucose", FitbitSedentaryMinutes = "FitbitSedentaryMinutes", FitbitActiveMinutes = "FitbitActiveMinutes", FitbitLightlyActiveMinutes = "FitbitLightlyActiveMinutes", @@ -81,6 +85,7 @@ export enum DailyDataType { AirQuality = "AirQuality", HomeAirQuality = "HomeAirQuality", WorkAirQuality = "WorkAirQuality", + BloodGlucose = "BloodGlucose", HealthConnectRestingHeartRate = "HealthConnectRestingHeartRate", HealthConnectTotalSleepMinutes = "HealthConnectTotalSleepMinutes", HealthConnectLightSleepMinutes = "HealthConnectLightSleepMinutes", @@ -94,6 +99,9 @@ export enum DailyDataType { HealthConnectTotalCaloriesBurned = "HealthConnectTotalCaloriesBurned", HealthConnectTherapyMinutes = "HealthConnectTherapyMinutes", HealthConnectMindfulMinutes = "HealthConnectMindfulMinutes", + HealthConnectBloodGlucose = "HealthConnectBloodGlucose", + HealthConnectMinBloodGlucose = "HealthConnectMinBloodGlucose", + HealthConnectMaxBloodGlucose = "HealthConnectMaxBloodGlucose", OuraSteps = "OuraSteps", OuraRestingHeartRate = "OuraRestingHeartRate", OuraSleepMinutes = "OuraSleepMinutes", diff --git a/src/helpers/daily-data-types/apple-health.tsx b/src/helpers/daily-data-types/apple-health.tsx index 0755c82bd..9f7f5f195 100644 --- a/src/helpers/daily-data-types/apple-health.tsx +++ b/src/helpers/daily-data-types/apple-health.tsx @@ -1,13 +1,14 @@ import { FontAwesomeSvgIcon } from "react-fontawesome-svg-icon"; import { appleHealthActiveEnergyBurnedDataProvider, appleHealthDistanceDataProvider, appleHealthEstimatedAppleWatchWearTimeDataProvider, appleHealthFlightsClimbedDataProvider, appleHealthHeartRateRangeDataProvider, - appleHealthHrvDataProvider, appleHealthInBedDataProvider, appleHealthMaxHeartRateDataProvider, appleHealthRestingHeartRateDataProvider, + appleHealthHrvDataProvider, appleHealthInBedDataProvider, appleHealthMaxHeartRateDataProvider, appleHealthMinHeartRateDataProvider, appleHealthRestingHeartRateDataProvider, appleHealthSleepCoreDataProvider, appleHealthSleepDataProvider, appleHealthSleepDeepDataProvider, appleHealthSleepRemDataProvider, appleHealthStandTimeDataProvider, appleHealthStepsDataProvider, appleHealthWalkingHeartRateAverageDataProvider, - appleHealthNumberOfAlcoholicBeveragesDataProvider, appleHealthMindfulMinutesDataProvider, appleHealthTherapyMinutesDataProvider, appleHealthStepsWhileWearingDeviceDataProvider + appleHealthNumberOfAlcoholicBeveragesDataProvider, appleHealthMindfulMinutesDataProvider, appleHealthTherapyMinutesDataProvider, appleHealthStepsWhileWearingDeviceDataProvider, + appleHealthBloodGlucoseDataProvider, appleHealthMinBloodGlucoseDataProvider, appleHealthMaxBloodGlucoseDataProvider } from "../daily-data-providers"; import { DailyDataType, DailyDataTypeDefinition } from "../daily-data-types"; -import { faBed, faFireFlameCurved, faHeartbeat, faPerson, faRoute, faStairs, faCocktail, faHourglassHalf, faShoePrints, faHandBackFist } from "@fortawesome/free-solid-svg-icons"; +import { faBed, faCocktail, faDroplet, faFireFlameCurved, faHandBackFist, faHeartbeat, faHourglassHalf, faPerson, faRoute, faShoePrints, faStairs } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { defaultFormatter, distanceFormatter, distanceYAxisConverter, heartRateFormatter, hrvFormatter, minutesFormatter, minutesToHoursYAxisConverter } from "./formatters"; import { simpleAvailabilityCheck } from "./availability-check"; @@ -47,7 +48,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthHeartRateRange, dataProvider: appleHealthHeartRateRangeDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["HourlyMaximumHeartRate"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Hourly Minimum Heart Rate", "Hourly Maximum Heart Rate"], { requireAllTypes: true }), labelKey: "heart-rate-range", icon: , formatter: heartRateFormatter, @@ -65,16 +66,25 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthMaxHeartRate, dataProvider: appleHealthMaxHeartRateDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["HourlyMaximumHeartRate"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Hourly Maximum Heart Rate"]), labelKey: "max-heart-rate", icon: , formatter: heartRateFormatter, previewDataRange: [100, 180] }, + { + type: DailyDataType.AppleHealthMinHeartRate, + dataProvider: appleHealthMinHeartRateDataProvider, + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Hourly Minimum Heart Rate"]), + labelKey: "min-heart-rate", + icon: , + formatter: heartRateFormatter, + previewDataRange: [50, 80] + }, { type: DailyDataType.AppleHealthMindfulMinutes, dataProvider: appleHealthMindfulMinutesDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", "MindfulSession"), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", "Mindful Sessions"), labelKey: "mindful-minutes", icon: , formatter: value => formatNumberForLocale(value), @@ -83,7 +93,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthRestingHeartRate, dataProvider: appleHealthRestingHeartRateDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["RestingHeartRate"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Resting Heart Rate"]), labelKey: "resting-heart-rate", icon: , formatter: heartRateFormatter, @@ -92,7 +102,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthSleepMinutes, dataProvider: appleHealthSleepDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["SleepAnalysisInterval"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Sleep Analysis"]), labelKey: "sleep-time", icon: , formatter: minutesFormatter, @@ -102,7 +112,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthCoreSleepMinutes, dataProvider: appleHealthSleepCoreDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["SleepAnalysisInterval"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Sleep Analysis"]), labelKey: "core-sleep-time", icon: , formatter: minutesFormatter, @@ -112,7 +122,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthDeepSleepMinutes, dataProvider: appleHealthSleepDeepDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["SleepAnalysisInterval"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Sleep Analysis"]), labelKey: "deep-sleep-time", icon: , formatter: minutesFormatter, @@ -122,7 +132,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthRemSleepMinutes, dataProvider: appleHealthSleepRemDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["SleepAnalysisInterval"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Sleep Analysis"]), labelKey: "rem-sleep-time", icon: , formatter: minutesFormatter, @@ -132,7 +142,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthInBedMinutes, dataProvider: appleHealthInBedDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["SleepAnalysisInterval"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Sleep Analysis"]), labelKey: "in-bed-time", icon: , formatter: minutesFormatter, @@ -151,7 +161,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthSteps, dataProvider: appleHealthStepsDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["HourlySteps"]), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Hourly Steps"]), labelKey: "steps", icon: , formatter: defaultFormatter, @@ -160,7 +170,7 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ { type: DailyDataType.AppleHealthStepsWhileWearingDevice, dataProvider: appleHealthStepsWhileWearingDeviceDataProvider, - availabilityCheck: simpleAvailabilityCheck("AppleHealth", "HourlySteps"), + availabilityCheck: simpleAvailabilityCheck("AppleHealth", "Hourly Steps"), labelKey: "steps", icon: , formatter: defaultFormatter, @@ -202,6 +212,33 @@ let appleHealthTypeDefinitions: DailyDataTypeDefinition[] = [ icon: , formatter: defaultFormatter, previewDataRange: [0, 20] + }, + { + type: DailyDataType.AppleHealthBloodGlucose, + dataProvider: appleHealthBloodGlucoseDataProvider, + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Blood Glucose"]), + labelKey: "blood-glucose", + icon: , + formatter: defaultFormatter, + previewDataRange: [80, 160] + }, + { + type: DailyDataType.AppleHealthMinBloodGlucose, + dataProvider: appleHealthMinBloodGlucoseDataProvider, + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Blood Glucose"]), + labelKey: "blood-glucose-min", + icon: , + formatter: defaultFormatter, + previewDataRange: [60, 80] + }, + { + type: DailyDataType.AppleHealthMaxBloodGlucose, + dataProvider: appleHealthMaxBloodGlucoseDataProvider, + availabilityCheck: simpleAvailabilityCheck("AppleHealth", ["Blood Glucose"]), + labelKey: "blood-glucose-max", + icon: , + formatter: defaultFormatter, + previewDataRange: [160, 180] } ]; appleHealthTypeDefinitions.forEach((def) => { diff --git a/src/helpers/daily-data-types/combined.tsx b/src/helpers/daily-data-types/combined.tsx index 61f7aea32..6388b94a4 100644 --- a/src/helpers/daily-data-types/combined.tsx +++ b/src/helpers/daily-data-types/combined.tsx @@ -1,15 +1,15 @@ import { FontAwesomeSvgIcon } from "react-fontawesome-svg-icon"; import { DailyDataType, DailyDataTypeDefinition } from "../daily-data-types"; -import { faBed, faFireFlameCurved, faHeartbeat, faHourglassHalf, faShoePrints } from "@fortawesome/free-solid-svg-icons"; +import { faBed, faDroplet, faFireFlameCurved, faHeartbeat, faHourglassHalf, faShoePrints } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { defaultFormatter, heartRateFormatter, minutesFormatter, minutesToHoursYAxisConverter } from "./formatters"; -import { combinedActiveCaloriesBurnedDataProvider, combinedMindfulMinutesDataProvider, combinedRestingHeartRateDataProvider, combinedSleepDataProvider, combinedStepsDataProvider, combinedTherapyMinutesDataProvider } from "../daily-data-providers"; +import { combinedActiveCaloriesBurnedDataProvider, combinedBloodGlucoseDataProvider, combinedMindfulMinutesDataProvider, combinedRestingHeartRateDataProvider, combinedSleepDataProvider, combinedStepsDataProvider, combinedTherapyMinutesDataProvider } from "../daily-data-providers"; import { combinedAvailabilityCheck, sources } from "./availability-check"; import { formatNumberForLocale } from "../locale"; import { EXERCISE_SESSION_FILTERS } from "../daily-data-providers/health-connect-therapy-minutes"; const RESTING_HEART_RATE_SOURCES = sources( - ["AppleHealth", "RestingHeartRate"], + ["AppleHealth", "Resting Heart Rate"], ["Fitbit", "RestingHeartRate"], ["Garmin", "Daily"], ["Oura", "sleep"], @@ -17,7 +17,7 @@ const RESTING_HEART_RATE_SOURCES = sources( ); const STEPS_SOURCES = sources( - ["AppleHealth", "HourlySteps"], + ["AppleHealth", "Hourly Steps"], ["Fitbit", "Steps"], ["Garmin", "Daily"], ["HealthConnect", "steps-daily"], @@ -25,7 +25,7 @@ const STEPS_SOURCES = sources( ); const STEPS_WITH_GOOGLE_FIT_SOURCES = sources( - ["AppleHealth", "HourlySteps"], + ["AppleHealth", "Hourly Steps"], ["GoogleFit", "Steps"], ["Fitbit", "Steps"], ["Garmin", "Daily"], @@ -34,7 +34,7 @@ const STEPS_WITH_GOOGLE_FIT_SOURCES = sources( ); const SLEEP_MINUTES_SOURCES = sources( - ["AppleHealth", "SleepAnalysisInterval"], + ["AppleHealth", "Sleep Analysis"], ["Fitbit", ["SleepLevelRem", "SleepLevelLight", "SleepLevelDeep", "SleepLevelAsleep"]], ["Garmin", "Sleep"], ["Oura", "sleep"], @@ -42,7 +42,7 @@ const SLEEP_MINUTES_SOURCES = sources( ); const MINDFUL_MINUTES_SOURCES = sources( - ["AppleHealth", "MindfulSession"], + ["AppleHealth", "Mindful Sessions"], ["GoogleFit", "ActivitySegment"], ["HealthConnect", "mindfulness-sessions"] ); @@ -62,6 +62,11 @@ const ACTIVE_CALORIES_BURNED_SOURCES = sources( ["Oura", "daily-activity"] ); +const BLOOD_GLUCOSE_SOURCES = sources( + ["AppleHealth", "Blood Glucose"], + ["HealthConnect", "blood-glucose"] +); + const combinedTypeDefinitions: DailyDataTypeDefinition[] = [ { dataSource: "Unified", @@ -134,6 +139,16 @@ const combinedTypeDefinitions: DailyDataTypeDefinition[] = [ icon: , formatter: defaultFormatter, previewDataRange: [300, 500] + }, + { + dataSource: "Unified", + type: DailyDataType.BloodGlucose, + dataProvider: combinedBloodGlucoseDataProvider, + availabilityCheck: combinedAvailabilityCheck(BLOOD_GLUCOSE_SOURCES), + labelKey: "blood-glucose", + icon: , + formatter: defaultFormatter, + previewDataRange: [80, 160] } ]; export default combinedTypeDefinitions; diff --git a/src/helpers/daily-data-types/health-connect.tsx b/src/helpers/daily-data-types/health-connect.tsx index 87ece8805..4c5a03840 100644 --- a/src/helpers/daily-data-types/health-connect.tsx +++ b/src/helpers/daily-data-types/health-connect.tsx @@ -12,10 +12,13 @@ import { healthConnectStepsDataProvider, healthConnectTherapyMinutesDataProvider, healthConnectTotalCaloriesBurnedDataProvider, - healthConnectTotalSleepMinutesDataProvider + healthConnectTotalSleepMinutesDataProvider, + healthConnectBloodGlucoseDataProvider, + healthConnectMinBloodGlucoseDataProvider, + healthConnectMaxBloodGlucoseDataProvider } from '../daily-data-providers'; import { DailyDataType, DailyDataTypeDefinition } from '../daily-data-types'; -import { faBed, faFireFlameCurved, faHeartbeat, faHourglassHalf, faRoute, faShoePrints } from '@fortawesome/free-solid-svg-icons'; +import { faBed, faDroplet, faFireFlameCurved, faHeartbeat, faHourglassHalf, faRoute, faShoePrints } from '@fortawesome/free-solid-svg-icons'; import React from 'react'; import { defaultFormatter, distanceFormatter, distanceYAxisConverter, heartRateFormatter, minutesFormatter, minutesToHoursYAxisConverter } from './formatters'; import { simpleAvailabilityCheck } from './availability-check'; @@ -145,6 +148,33 @@ const healthConnectTypeDefinitions: DailyDataTypeDefinition[] = [ icon: , formatter: value => formatNumberForLocale(value), previewDataRange: [0, 120] + }, + { + type: DailyDataType.HealthConnectBloodGlucose, + dataProvider: healthConnectBloodGlucoseDataProvider, + availabilityCheck: simpleAvailabilityCheck("HealthConnect", ["blood-glucose"]), + labelKey: "blood-glucose", + icon: , + formatter: defaultFormatter, + previewDataRange: [80, 160] + }, + { + type: DailyDataType.HealthConnectMinBloodGlucose, + dataProvider: healthConnectMinBloodGlucoseDataProvider, + availabilityCheck: simpleAvailabilityCheck("HealthConnect", ["blood-glucose"]), + labelKey: "blood-glucose-min", + icon: , + formatter: defaultFormatter, + previewDataRange: [60, 80] + }, + { + type: DailyDataType.HealthConnectMaxBloodGlucose, + dataProvider: healthConnectMaxBloodGlucoseDataProvider, + availabilityCheck: simpleAvailabilityCheck("HealthConnect", ["blood-glucose"]), + labelKey: "blood-glucose-max", + icon: , + formatter: defaultFormatter, + previewDataRange: [160, 180] } ]; healthConnectTypeDefinitions.forEach((def) => {