package org.glukit.dexcom.sync; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import org.glukit.dexcom.sync.g4.DexcomG4Constants; import org.glukit.dexcom.sync.model.DexcomSyncData; import org.glukit.dexcom.sync.model.GlucoseReadRecord; import org.glukit.dexcom.sync.model.ManufacturingParameters; import org.glukit.dexcom.sync.model.UserEventRecord; import org.glukit.sync.AdapterService; import org.glukit.sync.api.*; import org.threeten.bp.Duration; import org.threeten.bp.Instant; import org.threeten.bp.LocalDateTime; import org.threeten.bp.ZoneId; import javax.annotation.Nullable; import java.util.Arrays; import java.util.Collection; import java.util.List; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Lists.newArrayList; import static org.glukit.dexcom.sync.model.UserEventRecord.UserEventType.CARBS; import static org.glukit.dexcom.sync.model.UserEventRecord.UserEventType.EXERCISE; import static org.glukit.dexcom.sync.model.UserEventRecord.UserEventType.INSULIN; import static org.glukit.sync.api.ExerciseSession.EMPTY_DESCRIPTION; import static org.glukit.sync.api.InsulinInjection.InsulinType.UNKNOWN; import static org.glukit.sync.api.InsulinInjection.UNAVAILABLE_INSULIN_NAME; /** * This service adapts Dexcom-specific physical models to higher-level models. * * @author alexandre.normand */ public class DexcomAdapterService implements AdapterService<DexcomSyncData> { private static final int GLUCOSE_DISPLAY_ONLY_MASK = 0x8000; private static final int GLUCOSE_READ_VALUE_MASK = 0x3ff; private static final float INVALID_GLUCOSE_RECORD_VALUE = -1.0f; public static final Predicate<GlucoseRead> VALID_READS_FILTER = new Predicate<GlucoseRead>() { @Override public boolean apply(@Nullable GlucoseRead input) { return input.getValue() != INVALID_GLUCOSE_RECORD_VALUE; } }; static final List<Integer> SPECIAL_GLUCOSE_VALUES = Arrays.asList(0, 1, 2, 3, 5, 6, 9, 10, 12); private static final Predicate<UserEventRecord> INSULIN_EVENT_FILTER = new Predicate<UserEventRecord>() { @Override public boolean apply(@Nullable UserEventRecord input) { return input.getEventType() == INSULIN; } }; private static final Predicate<UserEventRecord> EXERCISE_EVENT_FILTER = new Predicate<UserEventRecord>() { @Override public boolean apply(@Nullable UserEventRecord input) { return input.getEventType() == EXERCISE; } }; private static final Predicate<UserEventRecord> CARB_EVENT_FILTER = new Predicate<UserEventRecord>() { @Override public boolean apply(@Nullable UserEventRecord input) { return input.getEventType() == CARBS; } }; private Function<Long, Instant> DEXCOM_SYSTEM_TIME_TO_INSTANT = new Function<Long, Instant>() { @Nullable @Override public Instant apply(@Nullable Long secondsSinceDexcomEpoch) { checkNotNull(secondsSinceDexcomEpoch, "secondsSinceDexcomEpoch should be non-null."); return DexcomG4Constants.DEXCOM_EPOCH.plusSeconds(secondsSinceDexcomEpoch); } }; private Function<Long, LocalDateTime> DEXCOM_DISPLAY_TIME_TO_LOCAL_DATE_TIME = new Function<Long, LocalDateTime>() { @Nullable @Override public LocalDateTime apply(@Nullable Long secondsSinceDexcomEpoch) { checkNotNull(secondsSinceDexcomEpoch, "secondsSinceDexcomEpoch should be non-null."); Instant instantInUTC = DexcomG4Constants.DEXCOM_EPOCH.plusSeconds(secondsSinceDexcomEpoch); return LocalDateTime.ofInstant(instantInUTC, ZoneId.of("UTC")); } }; private Function<Integer, Float> DEXCOM_GLUCOSE_VALUE_TO_GLUCOSE_VALUE = new Function<Integer, Float>() { @Nullable @Override public Float apply(@Nullable Integer readValue) { checkNotNull(readValue, "readValue should be non-null."); boolean isDisplayOnly = (readValue & GLUCOSE_DISPLAY_ONLY_MASK) != 0; if (isDisplayOnly) { return INVALID_GLUCOSE_RECORD_VALUE; } else { int actualValue = readValue & GLUCOSE_READ_VALUE_MASK; if (SPECIAL_GLUCOSE_VALUES.contains(actualValue)) { return INVALID_GLUCOSE_RECORD_VALUE; } else { return readValue.floatValue(); } } } }; private Function<GlucoseReadRecord, GlucoseRead> DEXCOM_GLUCOSE_RECORD_TO_GLUCOSE_READ = new Function<GlucoseReadRecord, GlucoseRead>() { @Override public GlucoseRead apply(@javax.annotation.Nullable GlucoseReadRecord glucoseReadRecord) { checkNotNull(glucoseReadRecord, "glucoseReadRecord should be non-null"); Instant internalTimeUTC = DEXCOM_SYSTEM_TIME_TO_INSTANT.apply(glucoseReadRecord.getInternalSecondsSinceDexcomEpoch()); LocalDateTime displayTime = DEXCOM_DISPLAY_TIME_TO_LOCAL_DATE_TIME.apply(glucoseReadRecord.getLocalSecondsSinceDexcomEpoch()); float glucoseValue = DEXCOM_GLUCOSE_VALUE_TO_GLUCOSE_VALUE.apply(glucoseReadRecord.getGlucoseValueWithFlags()); // TODO: remove the hardcoded unit and replace by the actual unit as per the configuration settings of the // receiver return new GlucoseRead(internalTimeUTC, displayTime, glucoseValue, GlucoseRead.Unit.MG_PER_DL); } }; private Function<ManufacturingParameters, DeviceInfo> DEXCOM_MANUFACTURING_PARAMS_TO_DEVICE_INFO = new Function<ManufacturingParameters, DeviceInfo>() { @Override public DeviceInfo apply(@javax.annotation.Nullable ManufacturingParameters manufacturingParameters) { checkNotNull(manufacturingParameters, "manufacturingParameters should be non-null"); return new DeviceInfo(manufacturingParameters.getSerialNumber(), manufacturingParameters.getHardwareId(), manufacturingParameters.getHardwareRevision()); } }; private Function<UserEventRecord, InsulinInjection> USER_EVENT_RECORD_TO_INSULIN_INJECTION = new Function<UserEventRecord, InsulinInjection>() { @Override public InsulinInjection apply(@javax.annotation.Nullable UserEventRecord insulinEvent) { checkNotNull(insulinEvent, "insulinEvent should be non-null"); checkArgument(insulinEvent.getEventType() == INSULIN); Instant internalTimeUTC = DEXCOM_SYSTEM_TIME_TO_INSTANT.apply(insulinEvent.getInternalSecondsSinceDexcomEpoch()); LocalDateTime localRecordedTime = DEXCOM_DISPLAY_TIME_TO_LOCAL_DATE_TIME.apply(insulinEvent.getLocalSecondsSinceDexcomEpoch()); LocalDateTime eventLocalTime = DEXCOM_DISPLAY_TIME_TO_LOCAL_DATE_TIME.apply(insulinEvent.getEventSecondsSinceDexcomEpoch()); float unitValue = insulinEvent.getEventValue() / 100.f; return new InsulinInjection(internalTimeUTC, localRecordedTime, eventLocalTime, unitValue, UNKNOWN, UNAVAILABLE_INSULIN_NAME); } }; private Function<UserEventRecord, FoodEvent> USER_EVENT_RECORD_TO_FOOD_EVENT = new Function<UserEventRecord, FoodEvent>() { @Override public FoodEvent apply(@javax.annotation.Nullable UserEventRecord carbEvent) { checkNotNull(carbEvent, "insulinEvent should be non-null"); checkArgument(carbEvent.getEventType() == CARBS); Instant internalTimeUTC = DEXCOM_SYSTEM_TIME_TO_INSTANT.apply(carbEvent.getInternalSecondsSinceDexcomEpoch()); LocalDateTime localRecordedTime = DEXCOM_DISPLAY_TIME_TO_LOCAL_DATE_TIME.apply(carbEvent.getLocalSecondsSinceDexcomEpoch()); LocalDateTime eventLocalTime = DEXCOM_DISPLAY_TIME_TO_LOCAL_DATE_TIME.apply(carbEvent.getEventSecondsSinceDexcomEpoch()); float unitValue = carbEvent.getEventValue(); return new FoodEvent(internalTimeUTC, localRecordedTime, eventLocalTime, unitValue, 0f); } }; private Function<UserEventRecord.ExerciseIntensity, ExerciseSession.Intensity> DEXCOM_EXERCISE_INTENSITY_TO_INTENSITY = new Function<UserEventRecord.ExerciseIntensity, ExerciseSession.Intensity>() { @Nullable @Override public ExerciseSession.Intensity apply(@Nullable UserEventRecord.ExerciseIntensity exerciseIntensity) { checkNotNull(exerciseIntensity, "exerciseIntensity should be non-null"); switch (exerciseIntensity) { case LIGHT: return ExerciseSession.Intensity.LIGHT; case MEDIUM: return ExerciseSession.Intensity.MEDIUM; case HEAVY: return ExerciseSession.Intensity.HEAVY; default: return null; } } }; private Function<UserEventRecord, ExerciseSession> USER_EVENT_RECORD_TO_EXERCISE_SESSION = new Function<UserEventRecord, ExerciseSession>() { @Override public ExerciseSession apply(@javax.annotation.Nullable UserEventRecord exerciseSession) { checkNotNull(exerciseSession, "exerciseSession should be non-null"); checkArgument(exerciseSession.getEventType() == EXERCISE); Instant internalTimeUTC = DEXCOM_SYSTEM_TIME_TO_INSTANT.apply(exerciseSession.getInternalSecondsSinceDexcomEpoch()); LocalDateTime localRecordedTime = DEXCOM_DISPLAY_TIME_TO_LOCAL_DATE_TIME.apply(exerciseSession.getLocalSecondsSinceDexcomEpoch()); LocalDateTime eventLocalTime = DEXCOM_DISPLAY_TIME_TO_LOCAL_DATE_TIME.apply(exerciseSession.getEventSecondsSinceDexcomEpoch()); long duration = exerciseSession.getEventValue(); UserEventRecord.ExerciseIntensity exerciseIntensity = UserEventRecord.ExerciseIntensity.fromId(exerciseSession.getEventSubType()); ExerciseSession.Intensity intensity = DEXCOM_EXERCISE_INTENSITY_TO_INTENSITY.apply(exerciseIntensity); return new ExerciseSession(internalTimeUTC, localRecordedTime, eventLocalTime, intensity, Duration.ofMinutes(duration), EMPTY_DESCRIPTION); } }; @Override public SyncData convertData(DexcomSyncData source) { List<GlucoseRead> glucoseReads = newArrayList(Collections2.filter(Collections2.transform(source.getGlucoseReads(), DEXCOM_GLUCOSE_RECORD_TO_GLUCOSE_READ), VALID_READS_FILTER)); DeviceInfo deviceInfo = DEXCOM_MANUFACTURING_PARAMS_TO_DEVICE_INFO.apply(source.getManufacturingParameters()); Collection<UserEventRecord> insulinEvents = Collections2.filter(source.getUserEvents(), INSULIN_EVENT_FILTER); Collection<UserEventRecord> exerciseEvents = Collections2.filter(source.getUserEvents(), EXERCISE_EVENT_FILTER); Collection<UserEventRecord> carbEvents = Collections2.filter(source.getUserEvents(), CARB_EVENT_FILTER); List<InsulinInjection> injections = newArrayList(Collections2.transform(insulinEvents, USER_EVENT_RECORD_TO_INSULIN_INJECTION)); List<ExerciseSession> exerciseSessions = newArrayList(Collections2.transform(exerciseEvents, USER_EVENT_RECORD_TO_EXERCISE_SESSION)); List<FoodEvent> foodEvents = newArrayList(Collections2.transform(carbEvents, USER_EVENT_RECORD_TO_FOOD_EVENT)); return new SyncData(glucoseReads, injections, foodEvents, exerciseSessions, deviceInfo, source.getUpdateTime()); } }