Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
551365c
feat: dexcom connector
Chin-eng Aug 3, 2026
e9774bd
feat: dexcom initial files
Chin-eng Aug 5, 2026
c38aa09
feat: dexcom topicdata
Chin-eng Aug 6, 2026
48f9da5
feat: offsets
Chin-eng Aug 6, 2026
9fb32ae
feat: dexcom EGV route
Chin-eng Aug 6, 2026
afdd0e6
feat: dexcom convertor: EGV
Chin-eng Aug 6, 2026
0807c02
feat: offsets
Chin-eng Aug 6, 2026
07ac68d
feat: dexcom route
Chin-eng Aug 6, 2026
2199d25
feat: dexcom requests
Chin-eng Aug 6, 2026
6d7b786
feat: gradle change
Chin-eng Aug 6, 2026
be4e226
feat: dexcom route
Chin-eng Aug 7, 2026
44b9c4d
feat: kafka dexcom connect files
Chin-eng Aug 8, 2026
ee29e02
feat: connector gradle kts
Chin-eng Aug 9, 2026
aa2e003
feat: dexcom dockerfile
Chin-eng Aug 9, 2026
18a3bdf
dexcom docker service
Chin-eng Aug 9, 2026
2616b55
feat: dexcom properties template
Chin-eng Aug 9, 2026
d7837fc
feat: dexcom egv import
Chin-eng Aug 9, 2026
2d8d28a
feat: dexcom egv
Chin-eng Aug 9, 2026
ed2c665
feat: dexcom user repo addition
Chin-eng Aug 9, 2026
59c8db1
feat: dexcomUser and User not found file
Chin-eng Aug 10, 2026
26cb46d
feat: git user files
Chin-eng Aug 10, 2026
d91ce32
feat:dexcom more user files
Chin-eng Aug 10, 2026
80c82cd
feat: dexcom config files
Chin-eng Aug 10, 2026
111ce26
feat: sequence extension
Chin-eng Aug 13, 2026
037ca17
feat: dexcom egv route update to mapctaching
Chin-eng Aug 14, 2026
fd1e110
feat: added sequence extension
Chin-eng Aug 14, 2026
f45ecc0
feat: dexcom events route
Chin-eng Aug 14, 2026
1149c6e
feat: dexcom calibrations
Chin-eng Aug 14, 2026
1cb11ba
feat: dexcom alerts
Chin-eng Aug 14, 2026
9141190
feat: dexcom devices
Chin-eng Aug 14, 2026
06edfc0
feat: dexcom daterange
Chin-eng Aug 14, 2026
341d826
feat: kafka offset manager
Chin-eng Aug 16, 2026
60f4f3d
feat: dexcom source task
Chin-eng Aug 16, 2026
bff9544
feat: dexcom abstartc rest source connector
Chin-eng Aug 17, 2026
8120e41
feat: dexcom source connector
Chin-eng Aug 17, 2026
e1c8484
feat: jackson empty consturctor
Chin-eng Aug 20, 2026
6196cb2
feat: dexcom caliberation endpoint
Chin-eng Aug 20, 2026
fd2afb1
feat: dexcom events connctor route
Chin-eng Aug 20, 2026
b914b9b
feat: alerts dexcom connector
Chin-eng Aug 20, 2026
53564f4
feat: dexcom data range connctor
Chin-eng Aug 20, 2026
ce83721
feat: dexcom device connector
Chin-eng Aug 20, 2026
81a66d8
fix: dexcom egv converter update
Chin-eng Sep 14, 2026
119f9b9
fix: dexcom alert converter update
Chin-eng Sep 14, 2026
ca8545a
fix: device converter update
Chin-eng Sep 14, 2026
2eed09f
fix: dexcom data range
Chin-eng Sep 14, 2026
d4874be
fix: calibration update
Chin-eng Sep 14, 2026
afcb028
fix: events converter update
Chin-eng Sep 14, 2026
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
49 changes: 49 additions & 0 deletions dexcom-library/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

group = 'org.radarbase'
version = '0.0.1'

apply plugin: 'maven-publish'

repositories {
// Use jcenter for resolving dependencies.
// You can declare any Maven/Ivy/file repository here.
mavenCentral()
}

dependencies {
// Use the Kotlin JDK 8 standard library.
implementation libs.kotlin.stdlib

implementation libs.okhttp

implementation libs.radar.schemas.commons

implementation libs.jackson.annotations

implementation libs.jackson.databind

implementation libs.avro

implementation libs.jackson.datatype.jsr310

implementation libs.slf4j.api

// Use the Kotlin test library.
testImplementation libs.kotlin.test

// Use the Kotlin JUnit integration.
testImplementation libs.kotlin.test.junit
}

project.afterEvaluate {
publishing {
publications {
library(MavenPublication) {
setGroupId "$group"
setArtifactId "dexcom-library"
version "$version"
from components.java
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.radarbase.dexcom.converter

import com.fasterxml.jackson.databind.JsonNode
import org.radarbase.dexcom.user.User
import org.radarcns.connector.dexcom.DexcomAlert
import org.radarcns.connector.dexcom.DexcomAlertName
import org.radarcns.connector.dexcom.DexcomAlertState
import org.radarcns.connector.dexcom.DexcomDisplayDevice
import org.radarcns.connector.dexcom.DexcomTransmitterGeneration
import org.radarcns.connector.dexcom.DexcomTransmitterGenerationVariant
import java.time.Instant

class DexcomAlertsConverter(
private val topic: String = "connect_dexcom_alert",
) : DexcomDataConverter {
override fun processRecords(
root: JsonNode,
user: User,
): Sequence<Result<TopicData>> {
val array = root.get("records")
?: return emptySequence()
return array.asSequence()
.mapCatching {
val systemTimeInstant = DexcomEGVConverter.parseDexcomTime(
it.get("systemTime").asText(),
)
TopicData(
key = user.observationKey,
topic = topic,
offset = systemTimeInstant.epochSecond,
value = it.toDexcomAlert(systemTimeInstant),
)
}
}

private fun JsonNode.toDexcomAlert(systemTimeInstant: Instant): DexcomAlert =
DexcomAlert.newBuilder().apply {
recordId = get("recordId").asText()
time = systemTimeInstant.epochSecond.toDouble()
displayTime = textOrNull("displayTime")
alertName = parseAlertName(textOrNull("alertName"))
alertState = parseAlertState(textOrNull("alertState"))
displayDevice = parseDisplayDevice(textOrNull("displayDevice"))
transmitterGeneration = parseTransmitterGeneration(textOrNull("transmitterGeneration"))
transmitterGenerationVariant =
parseTransmitterGenerationVariant(textOrNull("transmitterGenerationVariant"))
transmitterId = textOrNull("transmitterId").orEmpty()
displayApp = textOrNull("displayApp")
timeReceived = System.currentTimeMillis() / 1000.0
}.build()

private fun parseAlertName(value: String?): DexcomAlertName? =
when (value) {
null -> null
else -> when (value.lowercase()) {
"high" -> DexcomAlertName.HIGH
"low" -> DexcomAlertName.LOW
"rise" -> DexcomAlertName.RISE
"fall" -> DexcomAlertName.FALL
"outofrange" -> DexcomAlertName.OUT_OF_RANGE
"urgentlow" -> DexcomAlertName.URGENT_LOW
"urgentlowsoon" -> DexcomAlertName.URGENT_LOW_SOON
"noreadings" -> DexcomAlertName.NO_READINGS
else -> DexcomAlertName.UNKNOWN
}
}

private fun parseAlertState(value: String?): DexcomAlertState? =
when (value) {
null -> null
else -> when (value.lowercase()) {
"inactive" -> DexcomAlertState.INACTIVE
"activesnoozed" -> DexcomAlertState.ACTIVE_SNOOZED
"activealarming" -> DexcomAlertState.ACTIVE_ALARMING
else -> DexcomAlertState.UNKNOWN
}
}

private fun parseTransmitterGeneration(value: String?): DexcomTransmitterGeneration =
when (value?.lowercase()) {
"g6" -> DexcomTransmitterGeneration.G6
"g6+" -> DexcomTransmitterGeneration.G6_PLUS
"g6pro" -> DexcomTransmitterGeneration.G6_PRO
"g7" -> DexcomTransmitterGeneration.G7
else -> DexcomTransmitterGeneration.UNKNOWN
}

private fun parseTransmitterGenerationVariant(value: String?): DexcomTransmitterGenerationVariant =
when (value?.lowercase()) {
"d1+" -> DexcomTransmitterGenerationVariant.D1_PLUS
"g6" -> DexcomTransmitterGenerationVariant.G6
"g7" -> DexcomTransmitterGenerationVariant.G7
"g715day" -> DexcomTransmitterGenerationVariant.G7_15_DAY
else -> DexcomTransmitterGenerationVariant.UNKNOWN
}

private fun parseDisplayDevice(value: String?): DexcomDisplayDevice? =
when (value?.lowercase()) {
null -> null
"receiver" -> DexcomDisplayDevice.RECEIVER
"ios" -> DexcomDisplayDevice.IOS
"android" -> DexcomDisplayDevice.ANDROID
else -> DexcomDisplayDevice.UNKNOWN
}

private fun JsonNode.textOrNull(field: String): String? =
get(field)?.takeIf { !it.isNull }?.asText()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.radarbase.dexcom.converter

import com.fasterxml.jackson.databind.JsonNode
import org.radarbase.dexcom.user.User
import org.radarcns.connector.dexcom.DexcomCalibration
import org.radarcns.connector.dexcom.DexcomDisplayDevice
import org.radarcns.connector.dexcom.DexcomTransmitterGeneration
import org.radarcns.connector.dexcom.DexcomTransmitterGenerationVariant
import java.time.Instant

class DexcomCalibrationsConverter(
private val topic: String = "connect_dexcom_calibration",
) : DexcomDataConverter {
override fun processRecords(
root: JsonNode,
user: User,
): Sequence<Result<TopicData>> {
val array = root.get("records")
?: return emptySequence()
return array.asSequence()
.mapCatching {
val systemTimeInstant = DexcomEGVConverter.parseDexcomTime(
it.get("systemTime").asText(),
)
TopicData(
key = user.observationKey,
topic = topic,
offset = systemTimeInstant.epochSecond,
value = it.toDexcomCalibration(systemTimeInstant),
)
}
}

private fun JsonNode.toDexcomCalibration(systemTimeInstant: Instant): DexcomCalibration =
DexcomCalibration.newBuilder().apply {
recordId = get("recordId").asText()
time = systemTimeInstant.epochSecond.toDouble()
displayTime = textOrNull("displayTime")
unit = textOrNull("unit")
value = intOrNull("value")
displayDevice = parseDisplayDevice(textOrNull("displayDevice"))
transmitterId = textOrNull("transmitterId").orEmpty()
transmitterTicks = longOrNull("transmitterTicks")
transmitterGeneration = parseTransmitterGeneration(textOrNull("transmitterGeneration"))
transmitterGenerationVariant =
parseTransmitterGenerationVariant(textOrNull("transmitterGenerationVariant"))
timeReceived = System.currentTimeMillis() / 1000.0
}.build()

private fun parseTransmitterGeneration(value: String?): DexcomTransmitterGeneration =
when (value?.lowercase()) {
"g6" -> DexcomTransmitterGeneration.G6
"g6+" -> DexcomTransmitterGeneration.G6_PLUS
"g6pro" -> DexcomTransmitterGeneration.G6_PRO
"g7" -> DexcomTransmitterGeneration.G7
else -> DexcomTransmitterGeneration.UNKNOWN
}

private fun parseTransmitterGenerationVariant(value: String?): DexcomTransmitterGenerationVariant =
when (value?.lowercase()) {
"d1+" -> DexcomTransmitterGenerationVariant.D1_PLUS
"g6" -> DexcomTransmitterGenerationVariant.G6
"g7" -> DexcomTransmitterGenerationVariant.G7
"g715day" -> DexcomTransmitterGenerationVariant.G7_15_DAY
else -> DexcomTransmitterGenerationVariant.UNKNOWN
}

private fun parseDisplayDevice(value: String?): DexcomDisplayDevice? =
when (value?.lowercase()) {
null -> null
"receiver" -> DexcomDisplayDevice.RECEIVER
"ios" -> DexcomDisplayDevice.IOS
"android" -> DexcomDisplayDevice.ANDROID
else -> DexcomDisplayDevice.UNKNOWN
}

private fun JsonNode.textOrNull(field: String): String? =
get(field)?.takeIf { !it.isNull }?.asText()

private fun JsonNode.intOrNull(field: String): Int? =
get(field)?.takeIf { !it.isNull }?.asInt()

private fun JsonNode.longOrNull(field: String): Long? =
get(field)?.takeIf { !it.isNull }?.asLong()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.radarbase.dexcom.converter

import com.fasterxml.jackson.databind.JsonNode
import okhttp3.Headers
import org.radarbase.dexcom.request.DexcomRequestGenerator.Companion.JSON_READER
import org.radarbase.dexcom.request.RestRequest
import org.radarbase.dexcom.user.User
import java.time.Instant

interface DexcomDataConverter : RecordConverter {
fun processRecords(
root: JsonNode,
user: User,
): Sequence<Result<TopicData>>

override fun convert(
request: RestRequest,
headers: Headers,
data: ByteArray,
): List<TopicData> {
val node = JSON_READER.readTree(data)

return processRecords(node, request.user)
.mapNotNull { result ->
result.fold(
{ it },
{
RecordConverter.logger.error("Data conversion failed: ${it.message}")
null
},
)
}
.toList()
}

fun Instant.toEpoch(): Long = toEpochMilli() / 1000
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.radarbase.dexcom.converter

import com.fasterxml.jackson.databind.JsonNode
import org.radarbase.dexcom.user.User
import org.radarcns.connector.dexcom.DexcomDataRange
import org.radarcns.connector.dexcom.DexcomDataRangeMoment
import org.radarcns.connector.dexcom.DexcomDataRangeWindow
import java.time.Instant

/**
* /dataRange returns a single object (not records[]).
*/
class DexcomDataRangeConverter(
private val topic: String = "connect_dexcom_data_range",
) : DexcomDataConverter {
override fun processRecords(
root: JsonNode,
user: User,
): Sequence<Result<TopicData>> {
return sequenceOf(root).mapCatching {
val value = it.toDexcomDataRange()
val offset = it.offsetFromResponse()
TopicData(
key = user.observationKey,
topic = topic,
offset = offset,
value = value,
)
}
}

private fun JsonNode.offsetFromResponse(): Long {
val candidates = listOfNotNull(
get("egvs")?.get("end")?.get("systemTime")?.asText(),
get("events")?.get("end")?.get("systemTime")?.asText(),
get("calibrations")?.get("end")?.get("systemTime")?.asText(),
)
return candidates.firstNotNullOfOrNull { text ->
runCatching { DexcomEGVConverter.parseDexcomTime(text).epochSecond }.getOrNull()
} ?: Instant.now().epochSecond
}

private fun JsonNode.toDexcomDataRange(): DexcomDataRange =
DexcomDataRange.newBuilder().apply {
calibrations = get("calibrations")?.takeIf { !it.isNull }?.toWindow()
egvs = get("egvs")?.takeIf { !it.isNull }?.toWindow()
events = get("events")?.takeIf { !it.isNull }?.toWindow()
timeReceived = System.currentTimeMillis() / 1000.0
}.build()

private fun JsonNode.toWindow(): DexcomDataRangeWindow =
DexcomDataRangeWindow.newBuilder().apply {
start = get("start")?.takeIf { !it.isNull }?.toMoment() ?: emptyMoment()
end = get("end")?.takeIf { !it.isNull }?.toMoment() ?: emptyMoment()
}.build()

private fun JsonNode.toMoment(): DexcomDataRangeMoment =
DexcomDataRangeMoment.newBuilder().apply {
systemTime = systemTimeEpochOrNull()
displayTime = textOrNull("displayTime")
}.build()

private fun JsonNode.systemTimeEpochOrNull(): Double? =
get("systemTime")
?.takeIf { !it.isNull }
?.asText()
?.let { text ->
runCatching {
DexcomEGVConverter.parseDexcomTime(text).epochSecond.toDouble()
}.getOrNull()
}

private fun JsonNode.textOrNull(field: String): String? =
get(field)?.takeIf { !it.isNull }?.asText()

private fun emptyMoment() =
DexcomDataRangeMoment.newBuilder().build()
}
Loading