Skip to content
This repository has been archived by the owner on Apr 6, 2024. It is now read-only.

Commit

Permalink
Merge pull request #11 from burnedikt/9-carbohydrates-without-bolus-e…
Browse files Browse the repository at this point in the history
…q-snack-or-low-blood-sugar

support import of hypoglycaemia treatments / carb corrections
  • Loading branch information
burnedikt authored Sep 18, 2022
2 parents c50fe90 + b912075 commit 335cea9
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 45 deletions.
122 changes: 115 additions & 7 deletions __tests__/diasend-to-nightscout-treatments.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { diasendBolusRecordToNightscoutTreatment } from "../adapter";
import { BolusRecord, CarbRecord, DeviceData } from "../diasend";
import { CorrectionBolusTreatment, MealBolusTreatment } from "../nightscout";
import { identifyTreatments } from "../index";
import { diasendRecordToNightscoutTreatment } from "../adapter";
import { BolusRecord, CarbRecord, DeviceData, PatientRecord } from "../diasend";
import {
CarbCorrectionTreatment,
CorrectionBolusTreatment,
MealBolusTreatment,
} from "../nightscout";

const testDevice: DeviceData = {
manufacturer: "ACME",
Expand Down Expand Up @@ -40,7 +45,7 @@ describe("testing conversion of diasend patient data to nightscout treatments",
const device = testDevice;

// when converting the reading to a nightscout entry
const nightscoutTreatment = diasendBolusRecordToNightscoutTreatment(
const nightscoutTreatment = diasendRecordToNightscoutTreatment(
mealBolusRecord,
[mealBolusRecord, carbRecord],
device
Expand Down Expand Up @@ -94,7 +99,7 @@ describe("testing conversion of diasend patient data to nightscout treatments",
const device = testDevice;

// when converting the reading to a nightscout entry
const nightscoutTreatment = diasendBolusRecordToNightscoutTreatment(
const nightscoutTreatment = diasendRecordToNightscoutTreatment(
bolusRecord,
[bolusRecord, carbRecord],
device
Expand Down Expand Up @@ -141,7 +146,7 @@ describe("testing conversion of diasend patient data to nightscout treatments",
const device = testDevice;

// when converting the reading to a nightscout entry
const nightscoutTreatment = diasendBolusRecordToNightscoutTreatment(
const nightscoutTreatment = diasendRecordToNightscoutTreatment(
bolusRecord,
[bolusRecord],
device
Expand Down Expand Up @@ -183,7 +188,7 @@ describe("testing conversion of diasend patient data to nightscout treatments",
const device = testDevice;

// when converting the reading to a nightscout entry
const nightscoutTreatment = diasendBolusRecordToNightscoutTreatment(
const nightscoutTreatment = diasendRecordToNightscoutTreatment(
bolusRecord,
[bolusRecord],
device
Expand All @@ -198,4 +203,107 @@ describe("testing conversion of diasend patient data to nightscout treatments",
app: "diasend",
});
});

test("convert hypoglycaemia treatment", () => {
// given a hypoglycaemia treatment (which is essentially: Just carbs without any bolus)
const records: CarbRecord[] = [
{
type: "carb",
created_at: "2022-09-18T13:50:40",
value: "5",
unit: "g",
flags: [],
},
];

// When passing through the converter
const treatment = diasendRecordToNightscoutTreatment(
records[0],
records,
testDevice
);

// Then expect to obtain a hypo treatment
expect(treatment).toStrictEqual<CarbCorrectionTreatment>({
date: 1663501840000,
eventType: "Carb Correction",
carbs: 5,
device: "Test Pump (1111-22123)",
app: "diasend",
});
});

test("detect hypoglycaemia treatment with confusing meal bolus", () => {
// given a hypoglycaemia treatment (which is essentially: Just carbs without any bolus)
const records: PatientRecord[] = [
{
type: "glucose",
created_at: "2022-09-18T13:49:30",
value: 61,
unit: "mg/dl",
flags: [
{
flag: 123,
description: "Continous reading",
},
],
},
{
type: "carb",
created_at: "2022-09-18T13:50:40",
value: "7",
unit: "g",
flags: [],
},
{
type: "glucose",
created_at: "2022-09-18T13:54:29",
value: 56,
unit: "mg/dl",
flags: [
{
flag: 123,
description: "Continous reading",
},
],
},
// want to have a bolus here as well to ensure it's not mixed up with the hypo
{
type: "insulin_bolus",
created_at: "2022-09-18T14:08:38",
unit: "U",
total_value: 0.5,
spike_value: 0.5,
suggested: 0.5,
suggestion_overridden: "no",
suggestion_based_on_bg: "no",
suggestion_based_on_carb: "yes",
programmed_meal: 0.5,
flags: [
{
flag: 1035,
description: "Bolus type ezcarb",
},
],
},
{
type: "carb",
created_at: "2022-09-18T14:09:11",
value: "11",
unit: "g",
flags: [],
},
];

// When passing through the converter
const treatments = identifyTreatments(records, testDevice);

// Then expect to obtain a hypo treatment and a meal bolus
expect(treatments).toHaveLength(2);
expect(treatments[0].eventType).toBe("Carb Correction");
expect((treatments[0] as CarbCorrectionTreatment).carbs).toBe(7);
expect(treatments[1].eventType).toBe("Meal Bolus");
expect((treatments[1] as MealBolusTreatment).carbs).toBe(11);
expect((treatments[1] as MealBolusTreatment).insulin).toBe(0.5);
});
});
80 changes: 68 additions & 12 deletions adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {
CarbRecord,
DeviceData,
GlucoseRecord,
PatientRecord,
PumpSettings,
} from "./diasend";
import {
CarbCorrectionTreatment,
CorrectionBolusTreatment,
ManualGlucoseValueEntry,
MealBolusTreatment,
Expand Down Expand Up @@ -50,26 +52,80 @@ export function diasendGlucoseRecordToNightscoutEntry(
}
}

export function diasendBolusRecordToNightscoutTreatment(
record: BolusRecord,
// carbs should have been recorded within the next minute after a meal bolus
const diasendBolusCarbTimeDifferenceThresholdMilliseconds = 60 * 1000;
const nightscoutApp = "diasend";

function doCarbsBelongToBolus(
carbRecord: CarbRecord,
others: (CarbRecord | BolusRecord)[]
) {
return !!others
.filter<BolusRecord>(
(r): r is BolusRecord =>
r.type === "insulin_bolus" && "programmed_meal" in r
)
.find((bolusRecord) =>
isRecordCreatedWithinTimeSpan(
carbRecord,
bolusRecord,
diasendBolusCarbTimeDifferenceThresholdMilliseconds
)
);
}

function isRecordCreatedWithinTimeSpan(
r1: PatientRecord,
r2: PatientRecord,
timespanMilliseconds: number
) {
return (
dayjs(r1.created_at) > dayjs(r2.created_at) &&
dayjs(r1.created_at).diff(r2.created_at) <= timespanMilliseconds
);
}

export function diasendRecordToNightscoutTreatment(
record: BolusRecord | CarbRecord,
allRecords: (BolusRecord | CarbRecord)[],
device: DeviceData
): MealBolusTreatment | CorrectionBolusTreatment | undefined {
):
| MealBolusTreatment
| CorrectionBolusTreatment
| CarbCorrectionTreatment
| undefined {
// if there's a carb record, check if there's a preceeding (meal) bolus record
if (record.type == "carb") {
// if so, it's a meal / snack bolus and already handled
if (doCarbsBelongToBolus(record, allRecords)) return undefined;
// if not so, it's a hypoglycaemia treatment and we need to create a treatment for it
return {
eventType: "Carb Correction",
carbs: parseInt(record.value),
app: nightscoutApp,
date: new Date(record.created_at).getTime(),
device: `${device.model} (${device.serial})`,
};
}

const bolusRecord = record;
// for a (meal) bolus, find the corresponding carbs record, if any
// the carbs record is usually added ~1 minute later to diasend than the bolus for some reason
// if it's not a meal bolus, it's a correction bolus
const isMealBolus = "programmed_meal" in bolusRecord;
if (isMealBolus) {
const carbRecord = allRecords
.filter<CarbRecord>((r): r is CarbRecord => r.type === "carb")
.filter<CarbRecord>(
(record): record is CarbRecord => record.type === "carb"
)
.find(
(r) =>
// carbs should have been recorded within the next minute after bolus
new Date(r.created_at) > new Date(bolusRecord.created_at) &&
new Date(r.created_at).getTime() -
new Date(bolusRecord.created_at).getTime() <=
60 * 1000
// carbs should have been recorded within the next minute after bolus
(carbRecord) =>
isRecordCreatedWithinTimeSpan(
carbRecord,
bolusRecord,
diasendBolusCarbTimeDifferenceThresholdMilliseconds
)
);

const notesParts = [];
Expand All @@ -89,7 +145,7 @@ export function diasendBolusRecordToNightscoutTreatment(
insulin: bolusRecord.total_value,
carbs: !carbRecord ? undefined : parseInt(carbRecord.value),
notes: notesParts.length ? notesParts.join(", ") : undefined,
app: "diasend",
app: nightscoutApp,
date: new Date(bolusRecord.created_at).getTime(),
device: `${device.model} (${device.serial})`,
};
Expand All @@ -98,7 +154,7 @@ export function diasendBolusRecordToNightscoutTreatment(
return {
eventType: "Correction Bolus",
insulin: bolusRecord.programmed_bg_correction,
app: "diasend",
app: nightscoutApp,
date: new Date(bolusRecord.created_at).getTime(),
device: `${device.model} (${device.serial})`,
};
Expand Down
7 changes: 6 additions & 1 deletion diasend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface TokenResponse {
}

export interface BaseRecord {
type: "insulin_bolus" | "insulin_basal" | "glucose" | "carb";
created_at: string;
flags: { flag: number; description: string }[];
}
Expand Down Expand Up @@ -63,7 +64,11 @@ export interface BasalRecord extends BaseRecord {
value: number;
}

type PatientRecord = GlucoseRecord | BolusRecord | BasalRecord | CarbRecord;
export type PatientRecord =
| GlucoseRecord
| BolusRecord
| BasalRecord
| CarbRecord;

export interface DeviceData {
serial: string;
Expand Down
64 changes: 40 additions & 24 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
getAuthenticatedScrapingClient,
PumpSettings,
BasalRecord,
BaseRecord,
PatientRecord,
DeviceData,
} from "./diasend";
import {
reportEntriesToNightscout,
Expand All @@ -24,9 +27,10 @@ import {
updateProfile,
TimeBasedValue,
ProfileConfig,
CarbCorrectionTreatment,
} from "./nightscout";
import {
diasendBolusRecordToNightscoutTreatment,
diasendRecordToNightscoutTreatment,
diasendGlucoseRecordToNightscoutEntry,
diasendPumpSettingsToNightscoutProfile,
updateBasalProfile,
Expand All @@ -48,6 +52,36 @@ interface SyncDiasendDataToNightScoutArgs {
dateTo?: Date;
}

export function identifyTreatments(
records: PatientRecord[],
device: DeviceData
) {
return records
.filter<CarbRecord | BolusRecord>(
(record): record is CarbRecord | BolusRecord =>
["insulin_bolus", "carb"].includes(record.type)
)
.reduce<
(
| MealBolusTreatment
| CorrectionBolusTreatment
| CarbCorrectionTreatment
)[]
>((treatments, record, _index, allRecords) => {
const treatment = diasendRecordToNightscoutTreatment(
record,
allRecords,
device
);

if (treatment) {
treatments.push(treatment);
}

return treatments;
}, []);
}

async function syncDiasendDataToNightscout({
diasendUsername = config.diasend.username,
diasendPassword = config.diasend.password,
Expand Down Expand Up @@ -98,29 +132,11 @@ async function syncDiasendDataToNightscout({
diasendGlucoseRecordToNightscoutEntry(record, device)
);

// handle insulin boli
const nightscoutTreatments: Treatment[] = records
.filter<CarbRecord | BolusRecord>(
(record): record is CarbRecord | BolusRecord =>
["insulin_bolus", "carb"].includes(record.type)
)
.reduce<(MealBolusTreatment | CorrectionBolusTreatment)[]>(
(treatments, record, _index, allRecords) => {
// we only care about boli
if (record.type === "carb") {
return treatments;
}

const treatment = diasendBolusRecordToNightscoutTreatment(
record,
allRecords,
device
);

return treatment ? [...treatments, treatment] : treatments;
},
[]
);
// handle insulin boli and carbs
const nightscoutTreatments: Treatment[] = identifyTreatments(
records,
device
);

// handle basal rates
const existingProfile = await nightscoutProfileLoader();
Expand Down
Loading

0 comments on commit 335cea9

Please sign in to comment.