forked from MORE-Platform/more-data-gateway
-
Notifications
You must be signed in to change notification settings - Fork 0
#195: Added Daily Steps and Epoch Steps to be stored in elastic #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
janoliver20
merged 2 commits into
develop
from
umm/develop/195-implementing-step-storeage
Dec 17, 2025
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
src/main/java/io/redlink/more/data/model/garmin/transformation/GarminStepData.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package io.redlink.more.data.model.garmin.transformation; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
|
|
||
| /** | ||
| * @param steps Number of steps per Daily or Epoch Summary | ||
| * @param stepsGoal Goal of steps count. Only present in Daily Summaries | ||
| * @param distanceInMeters Distance travelled in meters per Daily or Epoch Summary | ||
| */ | ||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| public record GarminStepData( | ||
| Integer steps, | ||
| Integer stepsGoal, | ||
| Double distanceInMeters | ||
| ) { | ||
| } | ||
33 changes: 33 additions & 0 deletions
33
src/main/java/io/redlink/more/data/transformers/garmin/steps/DailyStepDataTransformer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package io.redlink.more.data.transformers.garmin.steps; | ||
|
|
||
| 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.transformers.garmin.AbstractGarminTransformer; | ||
| import io.redlink.more.data.util.garmin.GarminStepDataUtils; | ||
| import org.apache.commons.lang3.Range; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.List; | ||
|
|
||
| @Component | ||
| public class DailyStepDataTransformer extends AbstractGarminTransformer { | ||
| @Override | ||
| public GarminSummaryType getSupportedType() { | ||
| return GarminSummaryType.DAILIES; | ||
| } | ||
|
|
||
| @Override | ||
| protected List<DataPoint> transformToDataPoint(List<Observation> observations, GarminDataPoint garminDataPoint) { | ||
| var data = GarminStepDataUtils.getStepData(super.endDateTime(garminDataPoint), garminDataPoint); | ||
| return super.transformGarminTimeDataToDataPoint(observations, garminDataPoint.getSummaryId(), DataType.DAILY_STEPS, data); | ||
| } | ||
|
|
||
| @Override | ||
| protected List<DataPoint> filterDataPointByTimeRange(List<Range<Instant>> validTimeRanges, List<DataPoint> dataBulk) { | ||
| return dataBulk; | ||
| } | ||
| } |
33 changes: 33 additions & 0 deletions
33
src/main/java/io/redlink/more/data/transformers/garmin/steps/EpochStepDataTransformer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package io.redlink.more.data.transformers.garmin.steps; | ||
|
|
||
| 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.transformers.garmin.AbstractGarminTransformer; | ||
| import io.redlink.more.data.util.garmin.GarminStepDataUtils; | ||
| import org.apache.commons.lang3.Range; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.List; | ||
|
|
||
| @Component | ||
| public class EpochStepDataTransformer extends AbstractGarminTransformer { | ||
| @Override | ||
| public GarminSummaryType getSupportedType() { | ||
| return GarminSummaryType.EPOCHS; | ||
| } | ||
|
|
||
| @Override | ||
| protected List<DataPoint> transformToDataPoint(List<Observation> observations, GarminDataPoint garminDataPoint) { | ||
| var data = GarminStepDataUtils.getStepData(super.endDateTime(garminDataPoint), garminDataPoint); | ||
| return super.transformGarminTimeDataToDataPoint(observations, garminDataPoint.getSummaryId(), DataType.EPOCH_STEPS, data); | ||
| } | ||
|
|
||
| @Override | ||
| protected List<DataPoint> filterDataPointByTimeRange(List<Range<Instant>> validTimeRanges, List<DataPoint> dataBulk) { | ||
| return dataBulk; | ||
| } | ||
| } |
15 changes: 15 additions & 0 deletions
15
src/main/java/io/redlink/more/data/util/garmin/GarminStepDataUtils.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package io.redlink.more.data.util.garmin; | ||
|
|
||
| import io.redlink.more.data.custom.model.GarminDataPoint; | ||
| import io.redlink.more.data.model.garmin.transformation.GarminStepData; | ||
| import io.redlink.more.data.model.garmin.transformation.GarminTimeData; | ||
| import io.redlink.more.data.util.MapperUtils; | ||
|
|
||
| import java.time.OffsetDateTime; | ||
|
|
||
| public class GarminStepDataUtils { | ||
| public static GarminTimeData<GarminStepData> getStepData(OffsetDateTime endDateTime, GarminDataPoint garminDataPoint) { | ||
| GarminStepData stepData = MapperUtils.convertValue(garminDataPoint, GarminStepData.class); | ||
| return new GarminTimeData<>(endDateTime.toInstant(), stepData); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
src/test/java/io/redlink/more/data/service/DailyStepDataTransformerTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| package io.redlink.more.data.service; | ||
|
|
||
| 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.scheduler.Event; | ||
| import io.redlink.more.data.transformers.garmin.steps.DailyStepDataTransformer; | ||
| import org.apache.commons.lang3.Range; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.mockito.Mockito.*; | ||
|
|
||
| class DailyStepDataTransformerTest { | ||
|
|
||
| private static class TestDailyStepDataTransformer extends DailyStepDataTransformer { | ||
| List<DataPoint> exposeFilterByTimeRange(List<Range<Instant>> validTimeRanges, List<DataPoint> dataBulk) { | ||
| return super.filterDataPointByTimeRange(validTimeRanges, dataBulk); | ||
| } | ||
| } | ||
|
|
||
| private final DailyStepDataTransformer transformer = new DailyStepDataTransformer(); | ||
| private final TestDailyStepDataTransformer testTransformer = new TestDailyStepDataTransformer(); | ||
|
|
||
| @Test | ||
| @DisplayName("getSupportedType returns DAILIES") | ||
| void getSupportedType_returnsDailies() { | ||
| assertThat(transformer.getSupportedType()).isEqualTo(GarminSummaryType.DAILIES); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("transform: returns one DAILY_STEPS DataPoint with end timestamp and step fields") | ||
| void transform_returnsDailyStepsDataPoint() { | ||
| // Given a Garmin datapoint with steps and a time window | ||
| GarminDataPoint garminDataPoint = mock(GarminDataPoint.class); | ||
| when(garminDataPoint.getSummaryId()).thenReturn("summary-steps-1"); | ||
| when(garminDataPoint.getStartTimeInSeconds()).thenReturn(1_700_000_000); // arbitrary | ||
| when(garminDataPoint.getDurationInSeconds()).thenReturn(3_600); // 1h | ||
| when(garminDataPoint.getStartTimeOffsetInSeconds()).thenReturn(0); | ||
|
|
||
| // Step-related fields used by MapperUtils.convertValue -> GarminStepData | ||
| when(garminDataPoint.getSteps()).thenReturn(12345); | ||
| when(garminDataPoint.getStepsGoal()).thenReturn(10000); | ||
| when(garminDataPoint.getDistanceInMeters()).thenReturn(7654.32); | ||
|
|
||
| Instant start = Instant.ofEpochSecond(1_700_000_000); | ||
| Instant end = start.plusSeconds(3_600); | ||
|
|
||
| Event schedule = new Event() | ||
| .setDateStart(start.minusSeconds(600)) | ||
| .setDateEnd(end.plusSeconds(600)); | ||
|
|
||
| Observation observation = new Observation( | ||
| 1, | ||
| null, | ||
| "Daily Steps Observation", | ||
| DataType.DAILY_STEPS.dataType, | ||
| null, | ||
| null, | ||
| schedule, | ||
| start, | ||
| start, | ||
| false, | ||
| false | ||
| ); | ||
|
|
||
| // When | ||
| List<DataPoint> result = transformer.transform(List.of(observation), garminDataPoint, start.minusSeconds(3600), end.plusSeconds(3600)); | ||
|
|
||
| // Then | ||
| assertThat(result).hasSize(1); | ||
| DataPoint dp = result.get(0); | ||
| assertThat(dp.dataType()).isEqualTo(DataType.DAILY_STEPS.name()); | ||
| assertThat(dp.observationId()).isEqualTo("1"); | ||
| // Effective time is the end of the window (endDateTime) | ||
| assertThat(dp.effectiveDateTime()).isEqualTo(end); | ||
|
|
||
| Map<String, Object> data = dp.data(); | ||
| assertThat(data).isNotNull(); | ||
| assertThat(data.get("steps")).isEqualTo(12345); | ||
| // verify renamed key is present and old key is absent | ||
| assertThat(data.get("stepsGoal")).isEqualTo(10000); | ||
| assertThat(data).doesNotContainKey("stepGoal"); | ||
| assertThat(data.get("distanceInMeters")).isEqualTo(7654.32); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("filterDataPointByTimeRange: returns input unchanged (no filtering)") | ||
| void filterDataPointByTimeRange_noFiltering() { | ||
| DataPoint a = new DataPoint("a", "1", "type", DataType.DAILY_STEPS.name(), Instant.now(), Instant.now(), Map.of()); | ||
| DataPoint b = new DataPoint("b", "1", "type", DataType.DAILY_STEPS.name(), Instant.now(), Instant.now(), Map.of()); | ||
| List<DataPoint> input = List.of(a, b); | ||
|
|
||
| List<DataPoint> out = testTransformer.exposeFilterByTimeRange(List.of(Range.of(Instant.EPOCH, Instant.now())), input); | ||
| assertThat(out).isEqualTo(input); | ||
| } | ||
| } |
97 changes: 97 additions & 0 deletions
97
src/test/java/io/redlink/more/data/service/EpochStepDataTransformerTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package io.redlink.more.data.service; | ||
|
|
||
| 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.scheduler.Event; | ||
| import io.redlink.more.data.transformers.garmin.steps.EpochStepDataTransformer; | ||
| import org.apache.commons.lang3.Range; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.mockito.Mockito.mock; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| class EpochStepDataTransformerTest { | ||
|
|
||
| private static class TestEpochStepDataTransformer extends EpochStepDataTransformer { | ||
| List<DataPoint> exposeFilterByTimeRange(List<Range<Instant>> validTimeRanges, List<DataPoint> dataBulk) { | ||
| return super.filterDataPointByTimeRange(validTimeRanges, dataBulk); | ||
| } | ||
| } | ||
|
|
||
| private final EpochStepDataTransformer transformer = new EpochStepDataTransformer(); | ||
| private final TestEpochStepDataTransformer testTransformer = new TestEpochStepDataTransformer(); | ||
|
|
||
| @Test | ||
| @DisplayName("getSupportedType returns EPOCHS") | ||
| void getSupportedType_returnsEpochs() { | ||
| assertThat(transformer.getSupportedType()).isEqualTo(GarminSummaryType.EPOCHS); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("transform: returns one EPOCH_STEPS DataPoint with end timestamp and step fields") | ||
| void transform_returnsEpochStepsDataPoint() { | ||
| GarminDataPoint garminDataPoint = mock(GarminDataPoint.class); | ||
| when(garminDataPoint.getSummaryId()).thenReturn("summary-steps-epoch-1"); | ||
| when(garminDataPoint.getStartTimeInSeconds()).thenReturn(1_700_100_000); | ||
| when(garminDataPoint.getDurationInSeconds()).thenReturn(300); // 5 min epoch | ||
| when(garminDataPoint.getStartTimeOffsetInSeconds()).thenReturn(0); | ||
|
|
||
| when(garminDataPoint.getSteps()).thenReturn(120); | ||
| when(garminDataPoint.getDistanceInMeters()).thenReturn(150.5); | ||
|
|
||
| Instant start = Instant.ofEpochSecond(1_700_100_000); | ||
| Instant end = start.plusSeconds(300); | ||
|
|
||
| Event schedule = new Event() | ||
| .setDateStart(start.minusSeconds(60)) | ||
| .setDateEnd(end.plusSeconds(60)); | ||
|
|
||
| Observation observation = new Observation( | ||
| 1, | ||
| null, | ||
| "Epoch Steps Observation", | ||
| DataType.EPOCH_STEPS.dataType, | ||
| null, | ||
| null, | ||
| schedule, | ||
| start, | ||
| start, | ||
| false, | ||
| false | ||
| ); | ||
|
|
||
| List<DataPoint> result = transformer.transform(List.of(observation), garminDataPoint, start.minusSeconds(600), end.plusSeconds(600)); | ||
|
|
||
| assertThat(result).hasSize(1); | ||
| DataPoint dp = result.get(0); | ||
| assertThat(dp.dataType()).isEqualTo(DataType.EPOCH_STEPS.name()); | ||
| assertThat(dp.observationId()).isEqualTo("1"); | ||
| assertThat(dp.effectiveDateTime()).isEqualTo(end); | ||
|
|
||
| Map<String, Object> data = dp.data(); | ||
| assertThat(data).isNotNull(); | ||
| assertThat(data.get("steps")).isEqualTo(120); | ||
| assertThat(data.get("stepsGoal")).isEqualTo(0); | ||
| assertThat(data.get("distanceInMeters")).isEqualTo(150.5); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("filterDataPointByTimeRange: returns input unchanged (no filtering)") | ||
| void filterDataPointByTimeRange_noFiltering() { | ||
| DataPoint a = new DataPoint("a", "1", "type", DataType.EPOCH_STEPS.name(), Instant.now(), Instant.now(), Map.of()); | ||
| DataPoint b = new DataPoint("b", "1", "type", DataType.EPOCH_STEPS.name(), Instant.now(), Instant.now(), Map.of()); | ||
| List<DataPoint> input = List.of(a, b); | ||
|
|
||
| List<DataPoint> out = testTransformer.exposeFilterByTimeRange(List.of(Range.of(Instant.EPOCH, Instant.now())), input); | ||
| assertThat(out).isEqualTo(input); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gut dass die Distanz eine Double ist, sonst könnte man sie nicht bis auf Micrometer Genauigkeit spezifizieren ;)