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:
commit
d20beee891
2 changed files with 114 additions and 1 deletions
|
@ -14,10 +14,14 @@ import app.aaps.core.interfaces.rx.events.EventPreferenceChange
|
||||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||||
import app.aaps.database.entities.GlucoseValue
|
import app.aaps.database.entities.GlucoseValue
|
||||||
import app.aaps.plugins.sync.R
|
import app.aaps.plugins.sync.R
|
||||||
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
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.SocketAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
@ -96,6 +100,7 @@ class GarminPlugin @Inject constructor(
|
||||||
registerEndpoint("/get", ::onGetBloodGlucose)
|
registerEndpoint("/get", ::onGetBloodGlucose)
|
||||||
registerEndpoint("/carbs", ::onPostCarbs)
|
registerEndpoint("/carbs", ::onPostCarbs)
|
||||||
registerEndpoint("/connect", ::onConnectPump)
|
registerEndpoint("/connect", ::onConnectPump)
|
||||||
|
registerEndpoint("/sgv.json", ::onSgv)
|
||||||
}
|
}
|
||||||
} else if (server != null) {
|
} else if (server != null) {
|
||||||
aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
|
aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
|
||||||
|
@ -277,4 +282,54 @@ class GarminPlugin @Inject constructor(
|
||||||
jo.addProperty("connected", loopHub.isConnected)
|
jo.addProperty("connected", loopHub.isConnected)
|
||||||
return jo.toString()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,10 @@ import org.mockito.Mockito.times
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||||
import org.mockito.Mockito.`when`
|
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.SocketAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
@ -28,6 +32,7 @@ import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.concurrent.locks.Condition
|
import java.util.concurrent.locks.Condition
|
||||||
|
import kotlin.ranges.LongProgression.Companion.fromClosedRange
|
||||||
|
|
||||||
class GarminPluginTest: TestBase() {
|
class GarminPluginTest: TestBase() {
|
||||||
private lateinit var gp: GarminPlugin
|
private lateinit var gp: GarminPlugin
|
||||||
|
@ -71,8 +76,9 @@ class GarminPluginTest: TestBase() {
|
||||||
"device" to "Test_Device")
|
"device" to "Test_Device")
|
||||||
|
|
||||||
private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue(
|
private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue(
|
||||||
|
id = 10 * timestamp.toEpochMilli(),
|
||||||
timestamp = timestamp.toEpochMilli(), raw = 90.0, value = value,
|
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
|
sourceSensor = GlucoseValue.SourceSensor.RANDOM
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -205,4 +211,56 @@ class GarminPluginTest: TestBase() {
|
||||||
verify(loopHub).connectPump()
|
verify(loopHub).connectPump()
|
||||||
verify(loopHub).isConnected
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue