Skip to content

Commit

Permalink
Merge pull request #53 from RADAR-base/new_garmin_data
Browse files Browse the repository at this point in the history
New garmin data integration
  • Loading branch information
yatharthranjan authored May 10, 2023
2 parents 2eb0bd3 + 4c7ce4e commit 316b012
Show file tree
Hide file tree
Showing 17 changed files with 579 additions and 30 deletions.
7 changes: 4 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
)
}

Expand Down
10 changes: 5 additions & 5 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions src/main/kotlin/org/radarbase/gateway/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Pair<SpecificRecord, SpecificRecord>> {
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"
}
}
Original file line number Diff line number Diff line change
@@ -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<Pair<SpecificRecord, SpecificRecord>> {
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"
}
}
Original file line number Diff line number Diff line change
@@ -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<Pair<SpecificRecord, SpecificRecord>> {
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<Pair<ObservationKey, GarminHeartRateSample>> {
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"
}
}
Original file line number Diff line number Diff line change
@@ -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<Pair<SpecificRecord, SpecificRecord>> {
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<Pair<ObservationKey, GarminRespiration>> {
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"
}
}
Original file line number Diff line number Diff line change
@@ -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<Pair<SpecificRecord, SpecificRecord>> {
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<Pair<ObservationKey, GarminPulseOx>> {
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"
}
}
Loading

0 comments on commit 316b012

Please sign in to comment.