diff --git a/build.gradle.kts b/build.gradle.kts index 290348f..592e2e9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,8 +62,8 @@ dependencies { runtimeOnly("org.glassfish.grizzly:grizzly-http-server-monitoring:$grizzlyVersion") val log4j2Version: String by project - runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:$log4j2Version") - runtimeOnly("org.apache.logging.log4j:log4j-api:$log4j2Version") + runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:$log4j2Version") + runtimeOnly("org.apache.logging.log4j:log4j-core:$log4j2Version") runtimeOnly("org.apache.logging.log4j:log4j-jul:$log4j2Version") val jedisVersion: String by project @@ -122,7 +122,8 @@ application { "-Dcom.sun.management.jmxremote.local.only=false", "-Dcom.sun.management.jmxremote.port=9010", "-Dcom.sun.management.jmxremote.authenticate=false", - "-Dcom.sun.management.jmxremote.ssl=false" + "-Dcom.sun.management.jmxremote.ssl=false", + "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager", ) } diff --git a/gradle.properties b/gradle.properties index e96f6a9..8049f71 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,13 +3,13 @@ dockerComposeStopContainers=true kotlinVersion=1.6.10 okhttp3Version=4.9.3 -radarJerseyVersion=0.8.1 +radarJerseyVersion=0.9.1 radarCommonsVersion=0.13.2 -radarSchemasVersion=0.7.6 +radarSchemasVersion=0.8.3 radarOauthClientVersion=0.8.0 -jacksonVersion=2.12.2 -slf4jVersion=1.7.32 -log4j2Version=2.17.0 +jacksonVersion=2.14.1 +slf4jVersion=2.0.7 +log4j2Version=2.20.0 kafkaVersion=2.8.1 confluentVersion=6.2.0 junitVersion=5.7.2 diff --git a/src/main/kotlin/org/radarbase/gateway/Config.kt b/src/main/kotlin/org/radarbase/gateway/Config.kt index 691109b..6cfaa25 100644 --- a/src/main/kotlin/org/radarbase/gateway/Config.kt +++ b/src/main/kotlin/org/radarbase/gateway/Config.kt @@ -65,9 +65,14 @@ data class GarminConfig( val respirationTopicName: String = "push_garmin_respiration", val activityDetailsSampleTopicName: String = "push_garmin_activity_detail_sample", val bodyBatterySampleTopicName: String = "push_garmin_body_battery_sample", - val heartRateSampleConverter: String = "push_garmin_heart_rate_sample", + val heartRateSampleTopicName: String = "push_garmin_heart_rate_sample", val sleepLevelTopicName: String = "push_garmin_sleep_level", - val stressLevelTopicName: String = "push_garmin_stress_level" + val stressLevelTopicName: String = "push_garmin_stress_level", + val sleepScoreTopicName: String = "push_garmin_sleep_score", + val bloodPressureTopicName: String = "push_garmin_blood_pressure", + val healthSnapshotTopicName: String = "push_garmin_health_snapshot_summary", + val heartRateVariabilityTopicName: String = "push_garmin_heart_rate_variability", + val heartRateVariabilitySampleTopicName: String = "push_garmin_heart_rate_variability_sample" ) { val userRepository: Class<*> = Class.forName(userRepositoryClass) diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivitiesGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivitiesGarminAvroConverter.kt index c33dddd..42172da 100644 --- a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivitiesGarminAvroConverter.kt +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivitiesGarminAvroConverter.kt @@ -29,6 +29,7 @@ open class ActivitiesGarminAvroConverter(topic: String = "push_integration_garmi timeReceived = Instant.now().toEpochMilli() / 1000.0 startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() activityType = node["activityType"]?.asText() + activityId = node["activityId"]?.asText() duration = node["durationInSeconds"]?.asInt() averageBikeCadence = node["averageBikeCadenceInRoundsPerMinute"]?.floatValue() averageHeartRate = node["averageHeartRateInBeatsPerMinute"]?.asInt() diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsGarminAvroConverter.kt index e89542c..b70d21b 100644 --- a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsGarminAvroConverter.kt +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/ActivityDetailsGarminAvroConverter.kt @@ -29,6 +29,7 @@ class ActivityDetailsGarminAvroConverter(topic: String = "push_integration_garmi timeReceived = Instant.now().toEpochMilli() / 1000.0 startTimeOffset = summary["startTimeOffsetInSeconds"]?.asInt() activityType = summary["activityType"]?.asText() + activityId = summary["activityId"]?.asText() duration = summary["durationInSeconds"]?.asInt() averageBikeCadence = summary["averageBikeCadenceInRoundsPerMinute"]?.floatValue() averageHeartRate = summary["averageHeartRateInBeatsPerMinute"]?.asInt() diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/BloodPressureGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/BloodPressureGarminAvroConverter.kt new file mode 100644 index 0000000..46afed0 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/BloodPressureGarminAvroConverter.kt @@ -0,0 +1,40 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import jakarta.ws.rs.BadRequestException +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminBloodPressureSummary +import java.time.Instant + +class BloodPressureGarminAvroConverter(topic: String = "push_integration_garmin_blood_pressure") : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) { + val activities = tree[ROOT] + if (activities == null || !activities.isArray) { + throw BadRequestException("The manual activities data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): SpecificRecord { + return GarminBloodPressureSummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + measurementTimeOffset = node["measurementTimeOffsetInSeconds"]?.asInt() + systolic = node["systolic"]?.asInt() + diastolic = node["diastolic"]?.asInt() + pulse = node["pulse"]?.asInt() + sourceType = node["sourceType"]?.asText() + }.build() + } + + companion object { + const val ROOT = "bloodPressure" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotGarminAvroConverter.kt new file mode 100644 index 0000000..e8b8044 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotGarminAvroConverter.kt @@ -0,0 +1,70 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import jakarta.ws.rs.BadRequestException +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminHealthSnapshotSummary +import java.time.Instant + +class HealthSnapshotGarminAvroConverter(topic: String = "push_integration_garmin_health_snapshot") : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) { + val activities = tree[ROOT] + if (activities == null || !activities.isArray) { + throw BadRequestException("The manual activities data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): SpecificRecord { + return GarminHealthSnapshotSummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + date = node["calendarDate"]?.asText() + duration = node["durationInSeconds"]?.asInt() + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + + for (summary in node["summaries"]) { + when (summary["summaryType"]?.asText()) { + "heart_rate" -> { + heartRateAverage = summary["avgValue"]?.floatValue() + heartRateMax = summary["maxValue"]?.floatValue() + heartRateMin = summary["minValue"]?.floatValue() + } + + "respiration" -> { + respirationAverage = summary["avgValue"]?.floatValue() + respirationMax = summary["maxValue"]?.floatValue() + respirationMin = summary["minValue"]?.floatValue() + } + + "stress" -> { + stressAverage = summary["avgValue"]?.floatValue() + stressMax = summary["maxValue"]?.floatValue() + stressMin = summary["minValue"]?.floatValue() + } + + "spo2" -> { + spo2Average = summary["avgValue"]?.floatValue() + spo2Max = summary["maxValue"]?.floatValue() + spo2Min = summary["minValue"]?.floatValue() + } + + "rmssd_hrv" -> rmssdHrvAverage = summary["avgValue"]?.floatValue() + + "sdrr_hrv" -> sdrrHrvAverage = summary["avgValue"]?.floatValue() + } + } + }.build() + } + + companion object { + const val ROOT = "healthSnapshot" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotHeartRateSampleGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotHeartRateSampleGarminAvroConverter.kt new file mode 100644 index 0000000..c46e30c --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotHeartRateSampleGarminAvroConverter.kt @@ -0,0 +1,54 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminHeartRateSample +import java.time.Instant + +class HealthSnapshotHeartRateSampleGarminAvroConverter( + topic: String = "push_integration_garmin_heart_rate_sample" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double + ): List> { + if (node == null) { + return emptyList() + } + + val summary = node.find { it["summaryType"]?.asText() == "heart_rate" } ?: return emptyList() + + return summary["epochSummaries"].fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminHeartRateSample.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.heartRate = value?.floatValue() + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = HealthSnapshotGarminAvroConverter.ROOT + const val SUB_NODE = "summaries" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotRespirationSampleGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotRespirationSampleGarminAvroConverter.kt new file mode 100644 index 0000000..c7c9e25 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotRespirationSampleGarminAvroConverter.kt @@ -0,0 +1,53 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminRespiration +import java.time.Instant + +class HealthSnapshotRespirationSampleGarminAvroConverter( + topic: String = "push_integration_garmin_respiration" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double + ): List> { + if (node == null) { + return emptyList() + } + val summary = node.find { it["summaryType"]?.asText() == "respiration" } ?: return emptyList() + + return summary["epochSummaries"].fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminRespiration.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.respiration = value?.floatValue() + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = HealthSnapshotGarminAvroConverter.ROOT + const val SUB_NODE = "summaries" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotSpO2SampleGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotSpO2SampleGarminAvroConverter.kt new file mode 100644 index 0000000..e4e3149 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotSpO2SampleGarminAvroConverter.kt @@ -0,0 +1,58 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminPulseOx +import java.time.Instant + +class HealthSnapshotSpO2SampleGarminAvroConverter( + topic: String = "push_integration_garmin_heart_rate_sample" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble(), + node["calendarDate"]?.asText() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double, + date: String? + ): List> { + if (node == null) { + return emptyList() + } + + val summary = node.find { it["summaryType"]?.asText() == "spo2" } ?: return emptyList() + + return summary["epochSummaries"].fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminPulseOx.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.date = date + this.spo2Value = value?.floatValue() + this.onDemand = true + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = HealthSnapshotGarminAvroConverter.ROOT + const val SUB_NODE = "summaries" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotStressSampleGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotStressSampleGarminAvroConverter.kt new file mode 100644 index 0000000..9643f10 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HealthSnapshotStressSampleGarminAvroConverter.kt @@ -0,0 +1,54 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminStressLevelSample +import java.time.Instant + +class HealthSnapshotStressSampleGarminAvroConverter( + topic: String = "push_integration_garmin_heart_rate_sample" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double + ): List> { + if (node == null) { + return emptyList() + } + + val summary = node.find { it["summaryType"]?.asText() == "stress" } ?: return emptyList() + + return summary["epochSummaries"].fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminStressLevelSample.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.stressLevel = value?.floatValue() + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = HealthSnapshotGarminAvroConverter.ROOT + const val SUB_NODE = "summaries" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateSampleGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateSampleGarminAvroConverter.kt index 854e490..c54fb3f 100644 --- a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateSampleGarminAvroConverter.kt +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateSampleGarminAvroConverter.kt @@ -46,7 +46,7 @@ class HeartRateSampleGarminAvroConverter( } companion object { - const val ROOT = "dailies" + const val ROOT = DailiesGarminAvroConverter.ROOT const val SUB_NODE = "timeOffsetHeartRateSamples" } } diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateVariabilityGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateVariabilityGarminAvroConverter.kt new file mode 100644 index 0000000..5e11304 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateVariabilityGarminAvroConverter.kt @@ -0,0 +1,40 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import jakarta.ws.rs.BadRequestException +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminHeartRateVariabilitySummary +import java.time.Instant + +class HeartRateVariabilityGarminAvroConverter(topic: String = "push_integration_garmin_hrv_summary") : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) { + val activities = tree[ROOT] + if (activities == null || !activities.isArray) { + throw BadRequestException("The manual activities data was invalid.") + } + } + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT] + .map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): SpecificRecord { + return GarminHeartRateVariabilitySummary.newBuilder().apply { + summaryId = node["summaryId"]?.asText() + time = node["startTimeInSeconds"].asDouble() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + date = node["calendarDate"]?.asText() + duration = node["durationInSeconds"]?.asInt() + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + lastNightAvg = node["lastNightAvg"]?.floatValue() + lastNight5MinHigh = node["lastNight5MinHigh"]?.floatValue() + }.build() + } + + companion object { + const val ROOT = "hrv" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateVariabilitySampleGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateVariabilitySampleGarminAvroConverter.kt new file mode 100644 index 0000000..a79ef86 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/HeartRateVariabilitySampleGarminAvroConverter.kt @@ -0,0 +1,52 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.kafka.ObservationKey +import org.radarcns.push.garmin.GarminHeartRateVariabilitySample +import java.time.Instant + +class HeartRateVariabilitySampleGarminAvroConverter( + topic: String = "push_integration_garmin_heart_rate_sample" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> + getSamples( + node[SUB_NODE], node["summaryId"].asText(), + user.observationKey, node["startTimeInSeconds"].asDouble() + ) + }.flatten() + } + + private fun getSamples( + node: JsonNode?, + summaryId: String, + observationKey: ObservationKey, + startTime: Double + ): List> { + if (node == null) { + return emptyList() + } + + return node.fields().asSequence().map { (key, value) -> + Pair( + observationKey, + GarminHeartRateVariabilitySample.newBuilder().apply { + this.summaryId = summaryId + this.time = startTime + key.toDouble() + this.timeReceived = Instant.now().toEpochMilli() / 1000.0 + this.hrvValue = value?.floatValue() + }.build() + ) + }.toList() + } + + companion object { + const val ROOT = HeartRateVariabilityGarminAvroConverter.ROOT + const val SUB_NODE = "hrvValues" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepScoreGarminAvroConverter.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepScoreGarminAvroConverter.kt new file mode 100644 index 0000000..3fdda70 --- /dev/null +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/converter/SleepScoreGarminAvroConverter.kt @@ -0,0 +1,40 @@ +package org.radarbase.push.integration.garmin.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.apache.avro.specific.SpecificRecord +import org.radarbase.push.integration.common.user.User +import org.radarcns.push.garmin.GarminSleepScoreSample +import java.time.Instant + +class SleepScoreGarminAvroConverter( + topic: String = "push_integration_garmin_sleep_score" +) : + GarminAvroConverter(topic) { + override fun validate(tree: JsonNode) = Unit + + override fun convert(tree: JsonNode, user: User): List> { + return tree[ROOT].map { node -> Pair(user.observationKey, getRecord(node)) } + } + + private fun getRecord(node: JsonNode): SpecificRecord { + return GarminSleepScoreSample.newBuilder().apply { + summaryId = summaryId + time = node["startTimeInSeconds"].asDouble() + startTimeOffset = node["startTimeOffsetInSeconds"]?.asInt() + timeReceived = Instant.now().toEpochMilli() / 1000.0 + totalDurationScoreQualifier = node[SUB_NODE]?.get("totalDuration")?.get(QUALIFIER)?.asText() + stressScoreQualifier = node[SUB_NODE]?.get("stress")?.get(QUALIFIER)?.asText() + awakeCountScoreQualifier = node[SUB_NODE]?.get("awakeCount")?.get(QUALIFIER)?.asText() + remPercentageScoreQualifier = node[SUB_NODE]?.get("remPercentage")?.get(QUALIFIER)?.asText() + restlessnessScoreQualifier = node[SUB_NODE]?.get("restlessness")?.get(QUALIFIER)?.asText() + lightPercentageScoreQualifier = node[SUB_NODE]?.get("lightPercentage")?.get(QUALIFIER)?.asText() + deepPercentageScoreQualifier = node[SUB_NODE]?.get("deepPercentage")?.get(QUALIFIER)?.asText() + }.build() + } + + companion object { + const val ROOT = "sleeps" + const val SUB_NODE = "sleepScores" + const val QUALIFIER = "qualifierKey" + } +} diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/resource/GarminPushEndpoint.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/resource/GarminPushEndpoint.kt index ed7ee60..79271af 100644 --- a/src/main/kotlin/org/radarbase/push/integration/garmin/resource/GarminPushEndpoint.kt +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/resource/GarminPushEndpoint.kt @@ -123,6 +123,30 @@ class GarminPushEndpoint( } } + @POST + @Path("bloodPressure") + fun addBloodPressure(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processBloodPressure(tree, user) + } + } + + @POST + @Path("healthSnapshot") + fun addHealthSnapshot(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processHealthSnapshot(tree, user) + } + } + + @POST + @Path("hrv") + fun addHeartRateVariability(): Response { + return processResponses { tree: JsonNode, user: User -> + healthApiService.processHeartRateVariability(tree, user) + } + } + /** * Processes responses for all users * @param function: The function to use to process data diff --git a/src/main/kotlin/org/radarbase/push/integration/garmin/service/GarminHealthApiService.kt b/src/main/kotlin/org/radarbase/push/integration/garmin/service/GarminHealthApiService.kt index 4148f53..549fc92 100644 --- a/src/main/kotlin/org/radarbase/push/integration/garmin/service/GarminHealthApiService.kt +++ b/src/main/kotlin/org/radarbase/push/integration/garmin/service/GarminHealthApiService.kt @@ -21,36 +21,29 @@ class GarminHealthApiService( ) { private val garminConfig: GarminConfig = config.pushIntegration.garmin - private val dailiesConverter = - DailiesGarminAvroConverter(garminConfig.dailiesTopicName) + private val dailiesConverter = DailiesGarminAvroConverter(garminConfig.dailiesTopicName) - private val activitiesConverter = - ActivitiesGarminAvroConverter(garminConfig.activitiesTopicName) + private val activitiesConverter = ActivitiesGarminAvroConverter(garminConfig.activitiesTopicName) - private val manualActivitiesConverter = - ManualActivitiesGarminAvroConverter(garminConfig.activitiesTopicName) + private val manualActivitiesConverter = ManualActivitiesGarminAvroConverter(garminConfig.activitiesTopicName) - private val activityDetailsConverter = - ActivityDetailsGarminAvroConverter(garminConfig.activityDetailsTopicName) + private val activityDetailsConverter = ActivityDetailsGarminAvroConverter(garminConfig.activityDetailsTopicName) private val epochsConverter = EpochsGarminAvroConverter(garminConfig.epochSummariesTopicName) private val sleepSummaryConverter = SleepsGarminAvroConverter(garminConfig.sleepsTopicName) - private val bodyCompsConverter = - BodyCompGarminAvroConverter(garminConfig.bodyCompositionsTopicName) + private val bodyCompsConverter = BodyCompGarminAvroConverter(garminConfig.bodyCompositionsTopicName) private val stressConverter = StressDetailsGarminAvroConverter(garminConfig.stressTopicName) - private val userMetricsConverter = - UserMetricsGarminAvroConverter(garminConfig.userMetricsTopicName) + private val userMetricsConverter = UserMetricsGarminAvroConverter(garminConfig.userMetricsTopicName) private val moveIQConverter = MoveIQGarminAvroConverter(garminConfig.moveIQTopicName) private val pulseOxConverter = PulseOxGarminAvroConverter(garminConfig.pulseOXTopicName) - private val respirationConverter = - RespirationGarminAvroConverter(garminConfig.respirationTopicName) + private val respirationConverter = RespirationGarminAvroConverter(garminConfig.respirationTopicName) private val activityDetailsSampleConverter = ActivityDetailsSampleGarminAvroConverter( garminConfig.activityDetailsSampleTopicName @@ -61,23 +54,45 @@ class GarminHealthApiService( ) private val heartRateSampleConverter = HeartRateSampleGarminAvroConverter( - garminConfig.heartRateSampleConverter + garminConfig.heartRateSampleTopicName ) private val sleepLevelConverter = SleepLevelGarminAvroConverter( garminConfig.sleepLevelTopicName ) - private val sleepPulseOxConverter = - SleepPulseOxGarminAvroConverter(garminConfig.pulseOXTopicName) + private val sleepPulseOxConverter = SleepPulseOxGarminAvroConverter(garminConfig.pulseOXTopicName) - private val sleepRespirationConverter = - SleepRespirationGarminAvroConverter(garminConfig.respirationTopicName) + private val sleepRespirationConverter = SleepRespirationGarminAvroConverter(garminConfig.respirationTopicName) + + private val sleepScoreConverter = SleepScoreGarminAvroConverter(garminConfig.sleepScoreTopicName) private val stressLevelConverter = StressLevelGarminAvroConverter( garminConfig.stressLevelTopicName ) + private val bloodPressureConverter = BloodPressureGarminAvroConverter(garminConfig.bloodPressureTopicName) + + private val healthSnapshotConverter = HealthSnapshotGarminAvroConverter(garminConfig.healthSnapshotTopicName) + + private val healthSnapshotHeartRateSampleConverter = + HealthSnapshotHeartRateSampleGarminAvroConverter(garminConfig.heartRateSampleTopicName) + + private val healthSnapshotRespirationSampleConverter = + HealthSnapshotRespirationSampleGarminAvroConverter(garminConfig.respirationTopicName) + + private val healthSnapshotSpO2SampleConverter = + HealthSnapshotSpO2SampleGarminAvroConverter(garminConfig.pulseOXTopicName) + + private val healthSnapshotStressSampleConverter = + HealthSnapshotStressSampleGarminAvroConverter(garminConfig.stressLevelTopicName) + + private val heartRateVariabilityConverter = + HeartRateVariabilityGarminAvroConverter(garminConfig.heartRateVariabilityTopicName) + + private val heartRateVariabilitySampleConverter = + HeartRateVariabilitySampleGarminAvroConverter(garminConfig.heartRateVariabilitySampleTopicName) + @Throws(IOException::class, BadRequestException::class) fun processDailies(tree: JsonNode, user: User): Response { val records = dailiesConverter.validateAndConvert(tree, user) @@ -135,6 +150,9 @@ class GarminHealthApiService( val respiration = sleepRespirationConverter.validateAndConvert(tree, user) producerPool.produce(sleepRespirationConverter.topic, respiration) + val sleepScore = sleepScoreConverter.validateAndConvert(tree, user) + producerPool.produce(sleepScoreConverter.topic, sleepScore) + return Response.ok().build() } @@ -186,4 +204,42 @@ class GarminHealthApiService( producerPool.produce(respirationConverter.topic, records) return Response.ok().build() } + + @Throws(IOException::class, BadRequestException::class) + fun processBloodPressure(tree: JsonNode, user: User): Response { + val records = bloodPressureConverter.validateAndConvert(tree, user) + producerPool.produce(bloodPressureConverter.topic, records) + return Response.ok().build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processHealthSnapshot(tree: JsonNode, user: User): Response { + val records = healthSnapshotConverter.validateAndConvert(tree, user) + producerPool.produce(healthSnapshotConverter.topic, records) + + val heartRate = healthSnapshotHeartRateSampleConverter.validateAndConvert(tree, user) + producerPool.produce(healthSnapshotHeartRateSampleConverter.topic, heartRate) + + val respiration = healthSnapshotRespirationSampleConverter.validateAndConvert(tree, user) + producerPool.produce(healthSnapshotRespirationSampleConverter.topic, respiration) + + val spo2 = healthSnapshotSpO2SampleConverter.validateAndConvert(tree, user) + producerPool.produce(healthSnapshotSpO2SampleConverter.topic, spo2) + + val stress = healthSnapshotStressSampleConverter.validateAndConvert(tree, user) + producerPool.produce(healthSnapshotStressSampleConverter.topic, stress) + + return Response.ok().build() + } + + @Throws(IOException::class, BadRequestException::class) + fun processHeartRateVariability(tree: JsonNode, user: User): Response { + val records = heartRateVariabilityConverter.validateAndConvert(tree, user) + producerPool.produce(heartRateVariabilityConverter.topic, records) + + val hrvSample = heartRateVariabilitySampleConverter.validateAndConvert(tree, user) + producerPool.produce(heartRateVariabilitySampleConverter.topic, hrvSample) + + return Response.ok().build() + } }