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
@@ -0,0 +1,52 @@
/*
* Copyright 2026 King's College London
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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.googleHealthActivityLevel
import org.radarcns.push.googlehealth.GoogleHealthActivityLevelType

/**
* Converts `activity-level` data points: the activity level the user sustained over an interval.
*/
class ActivityLevelGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConverter(topic) {
override fun convertDataPoint(
point: JsonNode,
user: User,
): List<Pair<SpecificRecord, SpecificRecord>> {
val data = point["activityLevel"] ?: return emptyList()
val (start, end) = parseInterval(data) ?: return emptyList()
val record = googleHealthActivityLevel {
time = epochSeconds(start)
timeReceived = nowEpochSeconds()
timeInterval = (end.epochSecond - start.epochSecond).toInt().coerceAtLeast(0)
level = mapLevel(data["activityLevelType"]?.asText())
}
return listOf(user.observationKey to record)
}

/** Google's `ACTIVITY_LEVEL_TYPE_UNSPECIFIED` and any future symbol map to `UNKNOWN`. */
private fun mapLevel(text: String?): GoogleHealthActivityLevelType = when (text) {
"SEDENTARY" -> GoogleHealthActivityLevelType.SEDENTARY
"LIGHTLY_ACTIVE" -> GoogleHealthActivityLevelType.LIGHTLY_ACTIVE
"MODERATELY_ACTIVE" -> GoogleHealthActivityLevelType.MODERATELY_ACTIVE
"VERY_ACTIVE" -> GoogleHealthActivityLevelType.VERY_ACTIVE
else -> GoogleHealthActivityLevelType.UNKNOWN
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2026 King's College London
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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.googleHealthFloors

/**
* Converts `floors` data points: elevation gained over an interval.
* Google documents `count` as an int64, serialised as a JSON string, `asInt` parses either form.
* The type supports true zeros, so a `count` of 0 is a real observation and is kept.
*/
class FloorsGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConverter(topic) {
override fun convertDataPoint(
point: JsonNode,
user: User,
): List<Pair<SpecificRecord, SpecificRecord>> {
val data = point["floors"] ?: return emptyList()
val (start, end) = parseInterval(data) ?: return emptyList()
val count = data["count"]?.asInt() ?: return emptyList()
val record = googleHealthFloors {
time = epochSeconds(start)
timeReceived = nowEpochSeconds()
timeInterval = (end.epochSecond - start.epochSecond).toInt().coerceAtLeast(0)
floors = count
}
return listOf(user.observationKey to record)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2026 King's College London
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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.googleHealthSedentaryPeriod

/**
* Converts `sedentary-period` data points: stretches during which the user was not moving while
* wearing the device. Google documents `interval` as this type's only field, so the record carries
* the interval alone.
*/
class SedentaryPeriodGoogleHealthAvroConverter(topic: String) : GoogleHealthAvroConverter(topic) {
override fun convertDataPoint(
point: JsonNode,
user: User,
): List<Pair<SpecificRecord, SpecificRecord>> {
val data = point["sedentaryPeriod"] ?: return emptyList()
val (start, end) = parseInterval(data) ?: return emptyList()
val record = googleHealthSedentaryPeriod {
time = epochSeconds(start)
timeReceived = nowEpochSeconds()
timeInterval = (end.epochSecond - start.epochSecond).toInt().coerceAtLeast(0)
}
return listOf(user.observationKey to record)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import org.radarcns.push.googlehealth.GoogleHealthHeartRate
import org.radarcns.push.googlehealth.GoogleHealthHeartRateVariability
import org.radarcns.push.googlehealth.GoogleHealthOxygenSaturation
import org.radarcns.push.googlehealth.GoogleHealthSteps
import org.radarcns.push.googlehealth.GoogleHealthFloors
import org.radarcns.push.googlehealth.GoogleHealthSedentaryPeriod
import org.radarcns.push.googlehealth.GoogleHealthActivityLevel
import org.radarcns.push.googlehealth.GoogleHealthDailyRestingHeartRate
import org.radarcns.push.googlehealth.GoogleHealthDailySleepTemperatureDerivations
import org.radarcns.push.googlehealth.GoogleHealthSleepClassic
Expand All @@ -28,6 +31,15 @@ inline fun googleHealthIrregularRhythmNotification(block: GoogleHealthIrregularR
inline fun googleHealthSteps(block: GoogleHealthSteps.Builder.() -> Unit): GoogleHealthSteps =
GoogleHealthSteps.newBuilder().apply(block).build()

inline fun googleHealthFloors(block: GoogleHealthFloors.Builder.() -> Unit): GoogleHealthFloors =
GoogleHealthFloors.newBuilder().apply(block).build()

inline fun googleHealthSedentaryPeriod(block: GoogleHealthSedentaryPeriod.Builder.() -> Unit): GoogleHealthSedentaryPeriod =
GoogleHealthSedentaryPeriod.newBuilder().apply(block).build()

inline fun googleHealthActivityLevel(block: GoogleHealthActivityLevel.Builder.() -> Unit): GoogleHealthActivityLevel =
GoogleHealthActivityLevel.newBuilder().apply(block).build()

inline fun googleHealthHeartRate(block: GoogleHealthHeartRate.Builder.() -> Unit): GoogleHealthHeartRate =
GoogleHealthHeartRate.newBuilder().apply(block).build()

Expand Down