Skip to content
Open
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 @@ -19,7 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.restingHeartRate
import org.radarbase.googlehealth.util.googleHealthDailyRestingHeartRate

class DailyRestingHeartRateGoogleHealthAvroConverter(topic: String) :
GoogleHealthAvroConverter(topic) {
Expand All @@ -36,7 +36,7 @@ class DailyRestingHeartRateGoogleHealthAvroConverter(topic: String) :
dateNode["month"].asInt(),
dateNode["day"].asInt(),
)
val record = restingHeartRate {
val record = googleHealthDailyRestingHeartRate {
date = isoDate
timeReceived = nowEpochSeconds()
restingHeartRate = bpm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.skinTemperature
import org.radarcns.connector.fitbit.FitbitSkinTemperatureLogType
import org.radarbase.googlehealth.util.googleHealthDailySleepTemperatureDerivations

class DailySleepTemperatureDerivationsGoogleHealthAvroConverter(topic: String) :
GoogleHealthAvroConverter(topic) {
Expand All @@ -31,12 +30,20 @@ class DailySleepTemperatureDerivationsGoogleHealthAvroConverter(topic: String) :
val data = point["dailySleepTemperatureDerivations"] ?: return emptyList()
val nightly = data["nightlyTemperatureCelsius"]?.takeIf { it.isNumber }?.floatValue() ?: return emptyList()
val baseline = data["baselineTemperatureCelsius"]?.takeIf { it.isNumber }?.floatValue() ?: return emptyList()
val time = parseDate(data) ?: return emptyList()
val record = skinTemperature {
this.time = epochSeconds(time)
// `date` is the civil date (in the user's timezone) the derivation is for — emit it
// directly as a yyyy-MM-dd string, like DailyRestingHeartRate, rather than as a
// UTC-midnight instant that could shift to the wrong local day downstream.
val dateNode = data["date"] ?: return emptyList()
val isoDate = String.format(
"%04d-%02d-%02d",
dateNode["year"].asInt(),
dateNode["month"].asInt(),
dateNode["day"].asInt(),
)
val record = googleHealthDailySleepTemperatureDerivations {
date = isoDate
timeReceived = nowEpochSeconds()
relativeTemperature = nightly - baseline
logType = FitbitSkinTemperatureLogType.UNKNOWN
}
return listOf(user.observationKey to record)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.activityHeartRate
import org.radarbase.googlehealth.util.exerciseHeartRate
import org.radarbase.googlehealth.util.activityLogRecord

class ExerciseGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConverter(topic) {
Expand All @@ -40,10 +40,14 @@ class ExerciseGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConvert
val energyKj = caloriesKcal?.let { (it * KCAL_TO_KJ).toFloat() }
val stepCount = metrics?.get("steps")?.takeIf { !it.isNull }?.asInt()
val avgHr = metrics?.get("averageHeartRateBeatsPerMinute")?.takeIf { !it.isNull }?.asInt()
val avgHeartRate = avgHr?.let { activityHeartRate { mean = it } }
val avgHeartRate = avgHr?.let { exerciseHeartRate { mean = it } }
val exerciseType = data["exerciseType"]?.asText()
val activityId = (point["name"]?.asText() ?: exerciseType ?: "")
.hashCode().toLong()
// The exercise (log) id is the last segment of the reconcile data point's `dataPointName`
// (e.g. users/{u}/dataTypes/exercise/dataPoints/7726011858216679720). Exercise is an
// identifiable data type, so this is always present — fail loudly rather than emit a
// fabricated id. It is also the id used to export the session's TCX track.
val activityId = point["dataPointName"]?.asText()?.substringAfterLast('/')?.toLongOrNull()
?: throw IllegalStateException("Exercise data point has no usable dataPointName log id: $point")
val record = activityLogRecord {
time = epochSeconds(start)
timeReceived = nowEpochSeconds()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.intradayHeartRate
import org.radarbase.googlehealth.util.googleHealthHeartRate

class HeartRateGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConverter(topic) {
override fun convertDataPoint(
Expand All @@ -29,7 +29,7 @@ class HeartRateGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConver
val data = point["heartRate"] ?: return emptyList()
val time = parseSampleTime(data) ?: return emptyList()
val bpm = data["beatsPerMinute"]?.asInt() ?: return emptyList()
val record = intradayHeartRate {
val record = googleHealthHeartRate {
this.time = epochSeconds(time)
timeReceived = nowEpochSeconds()
timeInterval = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.intradayHeartRateVariability
import org.radarbase.googlehealth.util.googleHealthHeartRateVariability

class HeartRateVariabilityGoogleHealthAvroConverter(topic: String) :
GoogleHealthAvroConverter(topic) {
Expand All @@ -31,18 +31,11 @@ class HeartRateVariabilityGoogleHealthAvroConverter(topic: String) :
val time = parseSampleTime(data) ?: return emptyList()
val rmssd = data["rootMeanSquareOfSuccessiveDifferencesMilliseconds"]?.floatValue()
?: return emptyList()
val record = intradayHeartRateVariability {
val record = googleHealthHeartRateVariability {
this.time = epochSeconds(time)
timeReceived = nowEpochSeconds()
this.rmssd = rmssd
coverage = UNAVAILABLE
highFrequency = UNAVAILABLE
lowFrequency = UNAVAILABLE
}
return listOf(user.observationKey to record)
}

companion object {
private const val UNAVAILABLE = 0.0f
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.intradaySpo2
import org.radarbase.googlehealth.util.googleHealthOxygenSaturation

class OxygenSaturationGoogleHealthAvroConverter(topic: String) :
GoogleHealthAvroConverter(topic) {
Expand All @@ -30,10 +30,10 @@ class OxygenSaturationGoogleHealthAvroConverter(topic: String) :
val data = point["oxygenSaturation"] ?: return emptyList()
val time = parseSampleTime(data) ?: return emptyList()
val pct = data["percentage"]?.floatValue() ?: return emptyList()
val record = intradaySpo2 {
val record = googleHealthOxygenSaturation {
this.time = epochSeconds(time)
timeReceived = nowEpochSeconds()
spo2 = pct
percentage = pct
}
return listOf(user.observationKey to record)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.breathingRate
import org.radarbase.googlehealth.util.googleHealthRespiratoryRateSleepSummary

class RespiratoryRateSleepSummaryGoogleHealthAvroConverter(topic: String) :
GoogleHealthAvroConverter(topic) {
Expand All @@ -33,7 +33,7 @@ class RespiratoryRateSleepSummaryGoogleHealthAvroConverter(topic: String) :
val full = data["fullSleepStats"]?.get("breathsPerMinute")?.floatValue() ?: UNAVAILABLE
val light = data["lightSleepStats"]?.get("breathsPerMinute")?.floatValue() ?: UNAVAILABLE
val rem = data["remSleepStats"]?.get("breathsPerMinute")?.floatValue() ?: UNAVAILABLE
val record = breathingRate {
val record = googleHealthRespiratoryRateSleepSummary {
this.time = epochSeconds(time)
timeReceived = nowEpochSeconds()
lightSleep = light
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.sleepClassic
import org.radarcns.connector.fitbit.FitbitSleepClassicLevel
import org.radarbase.googlehealth.util.googleHealthSleepClassic
import org.radarcns.push.googlehealth.GoogleHealthSleepClassicLevel
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
Expand All @@ -42,22 +42,24 @@ class SleepClassicGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroCon
?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return@mapNotNull null
val end = stage["endTime"]?.asText()
?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return@mapNotNull null
val record = sleepClassic {
dateTime = LOCAL_FMT.format(LocalDateTime.ofInstant(start, ZoneOffset.UTC))
// Render in the stage's own UTC offset so dateTime is the device's local wall clock
// (like Fitbit), not UTC. Google derives its civil fields the same way (physical + offset).
val startZone = ZoneOffset.ofTotalSeconds(parseUtcOffsetSeconds(stage["startUtcOffset"]?.asText()))
val record = googleHealthSleepClassic {
dateTime = LOCAL_FMT.format(LocalDateTime.ofInstant(start, startZone))
this.timeReceived = timeReceived
duration = (end.epochSecond - start.epochSecond).toInt().coerceAtLeast(0)
level = mapLevel(stageType)
efficiency = null
}
user.observationKey to record
}
}

private fun mapLevel(text: String?): FitbitSleepClassicLevel = when (text) {
"ASLEEP" -> FitbitSleepClassicLevel.ASLEEP
"RESTLESS" -> FitbitSleepClassicLevel.RESTLESS
"AWAKE" -> FitbitSleepClassicLevel.AWAKE
else -> FitbitSleepClassicLevel.UNKNOWN
private fun mapLevel(text: String?): GoogleHealthSleepClassicLevel = when (text) {
"ASLEEP" -> GoogleHealthSleepClassicLevel.ASLEEP
"RESTLESS" -> GoogleHealthSleepClassicLevel.RESTLESS
"AWAKE" -> GoogleHealthSleepClassicLevel.AWAKE
else -> GoogleHealthSleepClassicLevel.UNKNOWN
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.sleepStage
import org.radarcns.connector.fitbit.FitbitSleepStageLevel
import org.radarbase.googlehealth.util.googleHealthSleepStage
import org.radarcns.push.googlehealth.GoogleHealthSleepStageLevel
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
Expand All @@ -42,23 +42,24 @@ class SleepStageGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConve
?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return@mapNotNull null
val end = stage["endTime"]?.asText()
?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return@mapNotNull null
val record = sleepStage {
dateTime = LOCAL_FMT.format(LocalDateTime.ofInstant(start, ZoneOffset.UTC))

val startZone = ZoneOffset.ofTotalSeconds(parseUtcOffsetSeconds(stage["startUtcOffset"]?.asText()))
val record = googleHealthSleepStage {
dateTime = LOCAL_FMT.format(LocalDateTime.ofInstant(start, startZone))
this.timeReceived = timeReceived
duration = (end.epochSecond - start.epochSecond).toInt().coerceAtLeast(0)
level = mapLevel(stageType)
efficiency = null
}
user.observationKey to record
}
}

private fun mapLevel(text: String?): FitbitSleepStageLevel = when (text) {
"DEEP" -> FitbitSleepStageLevel.DEEP
"LIGHT" -> FitbitSleepStageLevel.LIGHT
"REM" -> FitbitSleepStageLevel.REM
"AWAKE" -> FitbitSleepStageLevel.AWAKE
else -> FitbitSleepStageLevel.UNKNOWN
private fun mapLevel(text: String?): GoogleHealthSleepStageLevel = when (text) {
"DEEP" -> GoogleHealthSleepStageLevel.DEEP
"LIGHT" -> GoogleHealthSleepStageLevel.LIGHT
"REM" -> GoogleHealthSleepStageLevel.REM
"AWAKE" -> GoogleHealthSleepStageLevel.AWAKE
else -> GoogleHealthSleepStageLevel.UNKNOWN
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.intradaySteps
import org.radarbase.googlehealth.util.googleHealthSteps

class StepsGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConverter(topic) {
override fun convertDataPoint(
Expand All @@ -29,7 +29,7 @@ class StepsGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConverter(
val data = point["steps"] ?: return emptyList()
val (start, end) = parseInterval(data) ?: return emptyList()
val count = data["count"]?.asInt() ?: return emptyList()
val record = intradaySteps {
val record = googleHealthSteps {
time = epochSeconds(start)
timeReceived = nowEpochSeconds()
timeInterval = (end.epochSecond - start.epochSecond).toInt().coerceAtLeast(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package org.radarbase.googlehealth.converter
import com.fasterxml.jackson.databind.JsonNode
import org.apache.avro.specific.SpecificRecord
import org.radarbase.googlehealth.user.User
import org.radarbase.googlehealth.util.intradayCalories
import org.radarbase.googlehealth.util.googleHealthTotalCalories
import java.time.Instant

class TotalCaloriesGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConverter(topic) {
Expand All @@ -30,13 +30,11 @@ class TotalCaloriesGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroCo
val start = point["startTime"]?.asText()?.let(Instant::parse) ?: return emptyList()
val end = point["endTime"]?.asText()?.let(Instant::parse) ?: return emptyList()
val kilocalories = point["totalCalories"]?.get("kcalSum")?.doubleValue() ?: return emptyList()
val record = intradayCalories {
val record = googleHealthTotalCalories {
time = epochSeconds(start)
timeReceived = nowEpochSeconds()
timeInterval = (end.epochSecond - start.epochSecond).toInt().coerceAtLeast(0)
calories = kilocalories
level = 0
mets = 0.0
}
return listOf(user.observationKey to record)
}
Expand Down
Loading