Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.redlink.more.data.api.app.v1.model.UpdatePermissionsRequestDTO;
import io.redlink.more.data.api.app.v1.webservices.GarminUserManagementApi;
import io.redlink.more.data.custom.model.GarminDataPoint;
import io.redlink.more.data.model.Alias;
import io.redlink.more.data.model.garmin.GarminSummaryType;
import io.redlink.more.data.service.garmin.GarminService;
import io.redlink.more.data.util.MapperUtils;
Expand All @@ -30,6 +31,11 @@ public class GarminApiV1Controller implements GarminUserManagementApi {

private final GarminService garminService;

private final List<Alias> aliases = List.of(
new Alias("measurementTimeInSeconds", "startTimeInSeconds"),
new Alias("measurementTimeOffsetInSeconds", "startTimeOffsetInSeconds")
);

public GarminApiV1Controller(GarminService garminService) {
this.garminService = garminService;
}
Expand Down Expand Up @@ -73,7 +79,11 @@ public ResponseEntity<Void> submitSummaries(String userAgent, String garminClien
.collect(Collectors.toMap(
entry -> GarminSummaryType.fromLabel(entry.getKey()),
entry -> ((List<?>) entry.getValue()).stream()
.map(item -> MapperUtils.convertValue(item, GarminDataPoint.class))
.map(item -> MapperUtils.convertValueWithAliases(
item,
GarminDataPoint.class,
aliases
))
.collect(Collectors.toList())
));
garminService.storeData(requestDataPoints);
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/redlink/more/data/model/Alias.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.redlink.more.data.model;

public record Alias(String key, String replacement) {
}
3 changes: 2 additions & 1 deletion src/main/java/io/redlink/more/data/model/DataType.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public enum DataType {
SLEEP_START("sleep_start"),
SLEEP_END("sleep_end"),
DAILY_STEPS("daily_steps"),
EPOCH_STEPS("epoch_steps");
EPOCH_STEPS("epoch_steps"),
BLOOD_PRESSURE("blood_pressure");

public final String dataType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public enum GarminSummaryType {
STRESSDETAILS,
PULSEOX,
SLEEPS,
HRV;
HRV,
BLOODPRESSURES;

public final String label;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.redlink.more.data.model.garmin.transformation;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
* Represents a blood pressure record in the Garmin ecosystem.
* This record encapsulates systolic and diastolic blood pressure readings,
* pulse information, and the source from which the data was obtained.
* <p>
* The {@link SourceType} enum defines the possible origins of the measurement,
* which can either be entered manually or recorded by a device.
*
* @param systolic The systolic blood pressure value measured in mmHg.
* @param diastolic The diastolic blood pressure value measured in mmHg.
* @param pulse The pulse rate measured in beats per minute (BPM).
* @param sourceType The origin of the blood pressure record (manual entry or device).
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record GarminBloodPressure(
Integer systolic,
Integer diastolic,
Integer pulse,
SourceType sourceType
) {
public enum SourceType {
MANUAL,
DEVICE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import io.redlink.more.data.model.garmin.GarminSummaryType;
import io.redlink.more.data.model.garmin.transformation.GarminTimeData;
import io.redlink.more.data.schedule.SchedulerUtils;
import io.redlink.more.data.util.DateTimeUtils;
import org.apache.commons.lang3.Range;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
Expand Down Expand Up @@ -58,16 +58,12 @@ protected List<DataPoint> transformGarminTimeDataToDataPoint(List<Observation> o
}

protected OffsetDateTime recordingTimestamp(GarminDataPoint garminDataPoint) {
Instant unixTimestamp = Instant.ofEpochSecond(garminDataPoint.getStartTimeInSeconds());
ZoneOffset offset = ZoneOffset.ofTotalSeconds(garminDataPoint.getStartTimeOffsetInSeconds());
return unixTimestamp.atOffset(offset);
return DateTimeUtils.offsetDateTimeFromEpochSeconds(garminDataPoint.getStartTimeInSeconds(), garminDataPoint.getStartTimeOffsetInSeconds());
}

protected OffsetDateTime endDateTime(GarminDataPoint garminDataPoint) {
int endTimestamp = garminDataPoint.getStartTimeInSeconds() + garminDataPoint.getDurationInSeconds();
Instant unixTimestamp = Instant.ofEpochSecond(endTimestamp);
ZoneOffset offset = ZoneOffset.ofTotalSeconds(garminDataPoint.getStartTimeOffsetInSeconds());
return unixTimestamp.atOffset(offset);
return DateTimeUtils.offsetDateTimeFromEpochSeconds(endTimestamp, garminDataPoint.getStartTimeOffsetInSeconds());
}

protected Range<Instant> getGarminDataPointTimeRange(GarminDataPoint garminDataPoint) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.redlink.more.data.transformers.garmin;

import io.redlink.more.data.custom.model.GarminDataPoint;
import io.redlink.more.data.model.DataPoint;
import io.redlink.more.data.model.DataType;
import io.redlink.more.data.model.Observation;
import io.redlink.more.data.model.garmin.GarminSummaryType;
import io.redlink.more.data.model.garmin.transformation.GarminBloodPressure;
import io.redlink.more.data.model.garmin.transformation.GarminTimeData;
import io.redlink.more.data.util.MapperUtils;
import org.apache.commons.lang3.Range;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.List;

@Component
public class BloodPressureTransformer extends AbstractGarminTransformer {
@Override
public GarminSummaryType getSupportedType() {
return GarminSummaryType.BLOODPRESSURES;
}

@Override
protected List<DataPoint> transformToDataPoint(List<Observation> observations, GarminDataPoint garminDataPoint) {
var data = extractBloodPressure(garminDataPoint);
return super.transformGarminTimeDataToDataPoint(observations, garminDataPoint.getSummaryId(), DataType.BLOOD_PRESSURE, data);
}

@Override
protected List<DataPoint> filterDataPointByTimeRange(List<Range<Instant>> validTimeRanges, List<DataPoint> dataBulk) {
return dataBulk;
}

private GarminTimeData<GarminBloodPressure> extractBloodPressure(GarminDataPoint garminDataPoint) {
GarminBloodPressure data = MapperUtils.convertValue(garminDataPoint, GarminBloodPressure.class);
var measurementTime = super.recordingTimestamp(garminDataPoint);
return new GarminTimeData<>(measurementTime.toInstant(), data);
}
}
13 changes: 13 additions & 0 deletions src/main/java/io/redlink/more/data/util/DateTimeUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.redlink.more.data.util;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

public class DateTimeUtils {
public static OffsetDateTime offsetDateTimeFromEpochSeconds(long epochSecond, int offset) {
Instant unixTimestamp = Instant.ofEpochSecond(epochSecond);
ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(offset);
return unixTimestamp.atOffset(zoneOffset);
}
}
20 changes: 20 additions & 0 deletions src/main/java/io/redlink/more/data/util/MapperUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.redlink.more.data.exception.BadRequestException;
import io.redlink.more.data.model.Alias;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MapperUtils {
public static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
Expand All @@ -29,6 +34,21 @@ public static <T> T convertValue(Object o, Class<T> c) {
return MAPPER.convertValue(o, c);
}

public static <T> T convertValueWithAliases(Object o, Class<T> targetClass, List<Alias> aliases) {
if (o == null) return null;

Map<String, Object> sourceMap = convertValue(o, Map.class);
Map<String, Object> mappedData = new HashMap<>(sourceMap);

aliases.forEach((alias) -> {
if (!mappedData.containsKey(alias.replacement()) && mappedData.containsKey(alias.key())) {
mappedData.put(alias.replacement(), mappedData.get(alias.key()));
}
});

return convertValue(mappedData, targetClass);
}

public static boolean isPrimitiveLike(Object value) {
return value instanceof String
|| value instanceof Number
Expand Down
20 changes: 20 additions & 0 deletions src/main/resources/openapi/CustomModelAPI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ components:
stepsGoal:
type: integer
description: Daily steps goal
systolic:
type: integer
format: int32
description: The systolic value of the blood pressure reading.
diastolic:
type: integer
format: int32
description: The diastolic value of the blood pressure reading.
pulse:
type: integer
format: int32
description: Pulse rate at the time the blood pressure reading
sourceType:
type: string
enum: [ MANUAL, DEVICE ]
default: "MANUAL"
description: |
This field is used to determine if blood pressure data was entered manually or synced from aGarmin Device. Possible values:
MANUAL: The user entered blood pressure information manually through a web form.
DEVICE: The user used a Garmin device to perform a blood pressure reading.
required:
- userId
- summaryId
Expand Down
Loading
Loading