Merge pull request #3079 from buessow/dev-sgv.json

Implement /sgv.json endpoint to support phone apps that use nightscou…
This commit is contained in:
Milos Kozak 2023-11-24 13:23:18 +01:00 committed by GitHub
commit d20beee891
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 114 additions and 1 deletions

View file

@ -14,10 +14,14 @@ import app.aaps.core.interfaces.rx.events.EventPreferenceChange
import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.database.entities.GlucoseValue
import app.aaps.plugins.sync.R
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
import java.net.SocketAddress
import java.net.URI
import java.time.Clock
@ -96,6 +100,7 @@ class GarminPlugin @Inject constructor(
registerEndpoint("/get", ::onGetBloodGlucose)
registerEndpoint("/carbs", ::onPostCarbs)
registerEndpoint("/connect", ::onConnectPump)
registerEndpoint("/sgv.json", ::onSgv)
}
} else if (server != null) {
aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
@ -277,4 +282,54 @@ class GarminPlugin @Inject constructor(
jo.addProperty("connected", loopHub.isConnected)
return jo.toString()
}
private fun glucoseSlopeMgDlPerMilli(glucose1: GlucoseValue, glucose2: GlucoseValue): Double {
return (glucose2.value - glucose1.value) / (glucose2.timestamp - glucose1.timestamp)
}
/** Returns glucose values in Nightscout/Xdrip format. */
@VisibleForTesting
@Suppress("UNUSED_PARAMETER")
fun onSgv(call: SocketAddress, uri: URI, requestBody: String?): CharSequence {
val count = getQueryParameter(uri,"count", 24L)
.toInt().coerceAtMost(1000).coerceAtLeast(1)
val briefMode = getQueryParameter(uri, "brief_mode", false)
// Guess a start time to get [count+1] readings. This is a heuristic that only works if we get readings
// every 5 minutes and we're not missing readings. We truncate in case we get more readings but we'll
// get less, e.g., in case we're missing readings for the last half hour. We get one extra reading,
// to compute the glucose delta.
val from = clock.instant().minus(Duration.ofMinutes(5L * (count + 1)))
val glucoseValues = loopHub.getGlucoseValues(from, false)
val joa = JsonArray()
for (i in 0 until count.coerceAtMost(glucoseValues.size)) {
val jo = JsonObject()
val glucose = glucoseValues[i]
if (!briefMode) {
jo.addProperty("_id", glucose.id.toString())
jo.addProperty("device", glucose.sourceSensor.toString())
val timestamp = Instant.ofEpochMilli(glucose.timestamp)
jo.addProperty("deviceString", timestamp.toString())
jo.addProperty("sysTime", timestamp.toString())
glucose.raw?.let { raw -> jo.addProperty("unfiltered", raw) }
}
jo.addProperty("date", glucose.timestamp)
jo.addProperty("sgv", glucose.value.roundToInt())
if (i + 1 < glucoseValues.size) {
// Compute the 5 minute delta.
val delta = 300_000.0 * glucoseSlopeMgDlPerMilli(glucoseValues[i + 1], glucose)
jo.addProperty("delta", BigDecimal(delta, MathContext(3, RoundingMode.HALF_UP)))
}
jo.addProperty("direction", glucose.trendArrow.text)
glucose.noise?.let { n -> jo.addProperty("noise", n) }
if (i == 0) {
when (loopHub.glucoseUnit) {
GlucoseUnit.MGDL -> jo.addProperty("units_hint", "mgdl")
GlucoseUnit.MMOL -> jo.addProperty("units_hint", "mmol")
}
}
joa.add(jo)
}
return joa.toString()
}
}

View file

@ -21,6 +21,10 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever
import java.net.SocketAddress
import java.net.URI
import java.time.Clock
@ -28,6 +32,7 @@ import java.time.Instant
import java.time.ZoneId
import java.time.temporal.ChronoUnit
import java.util.concurrent.locks.Condition
import kotlin.ranges.LongProgression.Companion.fromClosedRange
class GarminPluginTest: TestBase() {
private lateinit var gp: GarminPlugin
@ -71,8 +76,9 @@ class GarminPluginTest: TestBase() {
"device" to "Test_Device")
private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue(
id = 10 * timestamp.toEpochMilli(),
timestamp = timestamp.toEpochMilli(), raw = 90.0, value = value,
trendArrow = GlucoseValue.TrendArrow.FLAT, noise = null,
trendArrow = GlucoseValue.TrendArrow.FLAT, noise = 4.5,
sourceSensor = GlucoseValue.SourceSensor.RANDOM
)
@ -205,4 +211,56 @@ class GarminPluginTest: TestBase() {
verify(loopHub).connectPump()
verify(loopHub).isConnected
}
@Test
fun onSgv_NoGlucose() {
whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn(emptyList())
assertEquals("[]", gp.onSgv(mock(), createUri(mapOf()), null))
verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false)
}
@Test
fun onSgv_NoDelta() {
whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn(
listOf(createGlucoseValue(
clock.instant().minusSeconds(100L), 99.3)))
assertEquals(
"""[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol"}]""",
gp.onSgv(mock(), createUri(mapOf()), null))
verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false)
verify(loopHub).glucoseUnit
}
@Test
fun onSgv() {
whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
whenever(loopHub.getGlucoseValues(any(), eq(false))).thenAnswer { i ->
val from = i.getArgument<Instant>(0)
fromClosedRange(from.toEpochMilli(), clock.instant().toEpochMilli(), 300_000L)
.map(Instant::ofEpochMilli)
.mapIndexed { idx, ts -> createGlucoseValue(ts, 100.0+(10 * idx)) }.reversed()}
assertEquals(
"""[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"}]""",
gp.onSgv(mock(), createUri(mapOf("count" to "1")), null))
verify(loopHub).getGlucoseValues(
clock.instant().minusSeconds(600L), false)
assertEquals(
"""[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"},""" +
"""{"_id":"-2900000","device":"RANDOM","deviceString":"1969-12-31T23:55:10Z","sysTime":"1969-12-31T23:55:10Z","unfiltered":90.0,"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""",
gp.onSgv(mock(), createUri(mapOf("count" to "2")), null))
verify(loopHub).getGlucoseValues(
clock.instant().minusSeconds(900L), false)
assertEquals(
"""[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"},""" +
"""{"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""",
gp.onSgv(mock(), createUri(mapOf("count" to "2", "brief_mode" to "true")), null))
verify(loopHub, times(2)).getGlucoseValues(
clock.instant().minusSeconds(900L), false)
verify(loopHub, atLeastOnce()).glucoseUnit
}
}