Merge pull request #3008 from buessow/dev-garmin
Garmin: Fix glucose unites and widget support
This commit is contained in:
commit
9700ccd5a0
5 changed files with 256 additions and 14 deletions
|
@ -94,6 +94,8 @@ class GarminPlugin @Inject constructor(
|
|||
server?.close()
|
||||
server = HttpServer(aapsLogger, port).apply {
|
||||
registerEndpoint("/get", ::onGetBloodGlucose)
|
||||
registerEndpoint("/carbs", ::onPostCarbs)
|
||||
registerEndpoint("/connect", ::onConnectPump)
|
||||
}
|
||||
} else if (server != null) {
|
||||
aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
|
||||
|
@ -243,4 +245,36 @@ class GarminPlugin @Inject constructor(
|
|||
aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd")
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles carb notification from the device. */
|
||||
@VisibleForTesting
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onPostCarbs(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
|
||||
aapsLogger.info(LTag.GARMIN, "carbs from $caller, req: $uri")
|
||||
postCarbs(getQueryParameter(uri, "carbs", 0L).toInt())
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun postCarbs(carbs: Int) {
|
||||
if (carbs > 0) {
|
||||
loopHub.postCarbs(carbs)
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles pump connected notification that the user entered on the Garmin device. */
|
||||
@VisibleForTesting
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onConnectPump(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
|
||||
aapsLogger.info(LTag.GARMIN, "connect from $caller, req: $uri")
|
||||
val minutes = getQueryParameter(uri, "disconnectMinutes", 0L).toInt()
|
||||
if (minutes > 0) {
|
||||
loopHub.disconnectPump(minutes)
|
||||
} else {
|
||||
loopHub.connectPump()
|
||||
}
|
||||
|
||||
val jo = JsonObject()
|
||||
jo.addProperty("connected", loopHub.isConnected)
|
||||
return jo.toString()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,19 @@ interface LoopHub {
|
|||
/** Returns the factor by which the basal rate is currently raised (> 1) or lowered (< 1). */
|
||||
val temporaryBasal: Double
|
||||
|
||||
/** Tells the loop algorithm that the pump is physically connected. */
|
||||
fun connectPump()
|
||||
|
||||
/** Tells the loop algorithm that the pump will be physically disconnected
|
||||
* for the given number of minutes. */
|
||||
fun disconnectPump(minutes: Int)
|
||||
|
||||
/** Retrieves the glucose values starting at from. */
|
||||
fun getGlucoseValues(from: Instant, ascending: Boolean): List<GlucoseValue>
|
||||
|
||||
/** Notifies the system that carbs were eaten and stores the value. */
|
||||
fun postCarbs(carbohydrates: Int)
|
||||
|
||||
/** Stores hear rate readings that a taken and averaged of the given interval. */
|
||||
fun storeHeartRate(
|
||||
samplingStart: Instant, samplingEnd: Instant,
|
||||
|
|
|
@ -2,15 +2,26 @@ package app.aaps.plugins.sync.garmin
|
|||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import app.aaps.core.interfaces.aps.Loop
|
||||
import app.aaps.core.interfaces.constraints.ConstraintsChecker
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.iob.IobCobCalculator
|
||||
import app.aaps.core.interfaces.logging.AAPSLogger
|
||||
import app.aaps.core.interfaces.logging.LTag
|
||||
import app.aaps.core.interfaces.logging.UserEntryLogger
|
||||
import app.aaps.core.interfaces.profile.Profile
|
||||
import app.aaps.core.interfaces.profile.ProfileFunction
|
||||
import app.aaps.core.interfaces.pump.DetailedBolusInfo
|
||||
import app.aaps.core.interfaces.queue.CommandQueue
|
||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
import app.aaps.database.ValueWrapper
|
||||
import app.aaps.database.entities.EffectiveProfileSwitch
|
||||
import app.aaps.database.entities.GlucoseValue
|
||||
import app.aaps.database.entities.HeartRate
|
||||
import app.aaps.database.entities.OfflineEvent
|
||||
import app.aaps.database.entities.UserEntry
|
||||
import app.aaps.database.entities.ValueWithUnit
|
||||
import app.aaps.database.impl.AppRepository
|
||||
import app.aaps.database.impl.transactions.CancelCurrentOfflineEventIfAnyTransaction
|
||||
import app.aaps.database.impl.transactions.InsertOrUpdateHeartRateTransaction
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
|
@ -22,10 +33,15 @@ import javax.inject.Singleton
|
|||
* Interface to the functionality of the looping algorithm and storage systems.
|
||||
*/
|
||||
class LoopHubImpl @Inject constructor(
|
||||
private val aapsLogger: AAPSLogger,
|
||||
private val commandQueue: CommandQueue,
|
||||
private val constraintChecker: ConstraintsChecker,
|
||||
private val iobCobCalculator: IobCobCalculator,
|
||||
private val loop: Loop,
|
||||
private val profileFunction: ProfileFunction,
|
||||
private val repo: AppRepository,
|
||||
private val userEntryLogger: UserEntryLogger,
|
||||
private val sp: SP,
|
||||
) : LoopHub {
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -40,7 +56,9 @@ class LoopHubImpl @Inject constructor(
|
|||
|
||||
/** Returns the glucose unit (mg/dl or mmol/l) as selected by the user. */
|
||||
override val glucoseUnit: GlucoseUnit
|
||||
get() = profileFunction.getProfile()?.units ?: GlucoseUnit.MGDL
|
||||
get() = GlucoseUnit.fromText(sp.getString(
|
||||
app.aaps.core.utils.R.string.key_units,
|
||||
GlucoseUnit.MGDL.asText))
|
||||
|
||||
/** Returns the remaining bolus insulin on board. */
|
||||
override val insulinOnboard: Double
|
||||
|
@ -65,12 +83,51 @@ class LoopHubImpl @Inject constructor(
|
|||
return if (apsResult == null) Double.NaN else apsResult.percent / 100.0
|
||||
}
|
||||
|
||||
/** Tells the loop algorithm that the pump is physicallly connected. */
|
||||
override fun connectPump() {
|
||||
repo.runTransaction(
|
||||
CancelCurrentOfflineEventIfAnyTransaction(clock.millis())
|
||||
).subscribe()
|
||||
commandQueue.cancelTempBasal(true, null)
|
||||
userEntryLogger.log(UserEntry.Action.RECONNECT, UserEntry.Sources.GarminDevice)
|
||||
}
|
||||
|
||||
/** Tells the loop algorithm that the pump will be physically disconnected
|
||||
* for the given number of minutes. */
|
||||
override fun disconnectPump(minutes: Int) {
|
||||
currentProfile?.let { p ->
|
||||
loop.goToZeroTemp(minutes, p, OfflineEvent.Reason.DISCONNECT_PUMP)
|
||||
userEntryLogger.log(
|
||||
UserEntry.Action.DISCONNECT,
|
||||
UserEntry.Sources.GarminDevice,
|
||||
ValueWithUnit.Minute(minutes)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Retrieves the glucose values starting at from. */
|
||||
override fun getGlucoseValues(from: Instant, ascending: Boolean): List<GlucoseValue> {
|
||||
return repo.compatGetBgReadingsDataFromTime(from.toEpochMilli(), ascending)
|
||||
.blockingGet()
|
||||
}
|
||||
|
||||
/** Notifies the system that carbs were eaten and stores the value. */
|
||||
override fun postCarbs(carbohydrates: Int) {
|
||||
aapsLogger.info(LTag.GARMIN, "post $carbohydrates g carbohydrates")
|
||||
val carbsAfterConstraints =
|
||||
carbohydrates.coerceAtMost(constraintChecker.getMaxCarbsAllowed().value())
|
||||
userEntryLogger.log(
|
||||
UserEntry.Action.CARBS,
|
||||
UserEntry.Sources.GarminDevice,
|
||||
ValueWithUnit.Gram(carbsAfterConstraints)
|
||||
)
|
||||
val detailedBolusInfo = DetailedBolusInfo().apply {
|
||||
eventType = DetailedBolusInfo.EventType.CARBS_CORRECTION
|
||||
carbs = carbsAfterConstraints.toDouble()
|
||||
}
|
||||
commandQueue.bolus(detailedBolusInfo, null)
|
||||
}
|
||||
|
||||
/** Stores hear rate readings that a taken and averaged of the given interval. */
|
||||
override fun storeHeartRate(
|
||||
samplingStart: Instant, samplingEnd: Instant,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package app.aaps.plugins.sync.garmin
|
||||
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.resources.ResourceHelper
|
||||
import app.aaps.core.interfaces.rx.events.EventNewBG
|
||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
|
@ -9,14 +10,18 @@ import dagger.android.AndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.anyLong
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.atMost
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.Mockito.`when`
|
||||
import java.net.SocketAddress
|
||||
import java.net.URI
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
|
@ -113,4 +118,91 @@ class GarminPluginTest: TestBase() {
|
|||
verify(gp.newValue).signalAll()
|
||||
verify(loopHub).getGlucoseValues(from, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnGetBloodGlucose() {
|
||||
`when`(loopHub.isConnected).thenReturn(true)
|
||||
`when`(loopHub.insulinOnboard).thenReturn(3.14)
|
||||
`when`(loopHub.temporaryBasal).thenReturn(0.8)
|
||||
val from = getGlucoseValuesFrom
|
||||
`when`(loopHub.getGlucoseValues(from, true)).thenReturn(
|
||||
listOf(createGlucoseValue(Instant.ofEpochSecond(1_000))))
|
||||
val hr = createHeartRate(99)
|
||||
val uri = createUri(hr)
|
||||
val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null)
|
||||
assertEquals(
|
||||
"{\"encodedGlucose\":\"0A+6AQ==\"," +
|
||||
"\"remainingInsulin\":3.14," +
|
||||
"\"glucoseUnit\":\"mmoll\",\"temporaryBasalRate\":0.8," +
|
||||
"\"profile\":\"D\",\"connected\":true}",
|
||||
result.toString())
|
||||
verify(loopHub).getGlucoseValues(from, true)
|
||||
verify(loopHub).insulinOnboard
|
||||
verify(loopHub).temporaryBasal
|
||||
verify(loopHub).isConnected
|
||||
verify(loopHub).glucoseUnit
|
||||
verify(loopHub).storeHeartRate(
|
||||
Instant.ofEpochSecond(hr["hrStart"] as Long),
|
||||
Instant.ofEpochSecond(hr["hrEnd"] as Long),
|
||||
99,
|
||||
hr["device"] as String)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnGetBloodGlucose_Wait() {
|
||||
`when`(loopHub.isConnected).thenReturn(true)
|
||||
`when`(loopHub.insulinOnboard).thenReturn(3.14)
|
||||
`when`(loopHub.temporaryBasal).thenReturn(0.8)
|
||||
`when`(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
|
||||
val from = getGlucoseValuesFrom
|
||||
`when`(loopHub.getGlucoseValues(from, true)).thenReturn(
|
||||
listOf(createGlucoseValue(clock.instant().minusSeconds(330))))
|
||||
val params = createHeartRate(99).toMutableMap()
|
||||
params["wait"] = 10
|
||||
val uri = createUri(params)
|
||||
gp.newValue = mock(Condition::class.java)
|
||||
val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null)
|
||||
assertEquals(
|
||||
"{\"encodedGlucose\":\"/wS6AQ==\"," +
|
||||
"\"remainingInsulin\":3.14," +
|
||||
"\"glucoseUnit\":\"mmoll\",\"temporaryBasalRate\":0.8," +
|
||||
"\"profile\":\"D\",\"connected\":true}",
|
||||
result.toString())
|
||||
verify(gp.newValue).awaitNanos(anyLong())
|
||||
verify(loopHub, times(2)).getGlucoseValues(from, true)
|
||||
verify(loopHub).insulinOnboard
|
||||
verify(loopHub).temporaryBasal
|
||||
verify(loopHub).isConnected
|
||||
verify(loopHub).glucoseUnit
|
||||
verify(loopHub).storeHeartRate(
|
||||
Instant.ofEpochSecond(params["hrStart"] as Long),
|
||||
Instant.ofEpochSecond(params["hrEnd"] as Long),
|
||||
99,
|
||||
params["device"] as String)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPostCarbs() {
|
||||
val uri = createUri(mapOf("carbs" to "12"))
|
||||
assertEquals("", gp.onPostCarbs(mock(SocketAddress::class.java), uri, null))
|
||||
verify(loopHub).postCarbs(12)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnConnectPump_Disconnect() {
|
||||
val uri = createUri(mapOf("disconnectMinutes" to "20"))
|
||||
`when`(loopHub.isConnected).thenReturn(false)
|
||||
assertEquals("{\"connected\":false}", gp.onConnectPump(mock(SocketAddress::class.java), uri, null))
|
||||
verify(loopHub).disconnectPump(20)
|
||||
verify(loopHub).isConnected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnConnectPump_Connect() {
|
||||
val uri = createUri(mapOf("disconnectMinutes" to "0"))
|
||||
`when`(loopHub.isConnected).thenReturn(true)
|
||||
assertEquals("{\"connected\":true}", gp.onConnectPump(mock(SocketAddress::class.java), uri, null))
|
||||
verify(loopHub).connectPump()
|
||||
verify(loopHub).isConnected
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package app.aaps.plugins.sync.garmin
|
||||
|
||||
|
||||
import app.aaps.core.interfaces.aps.APSResult
|
||||
import app.aaps.core.interfaces.aps.Loop
|
||||
import app.aaps.core.interfaces.constraints.Constraint
|
||||
import app.aaps.core.interfaces.constraints.ConstraintsChecker
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.iob.IobCobCalculator
|
||||
|
@ -10,13 +10,19 @@ import app.aaps.core.interfaces.iob.IobTotal
|
|||
import app.aaps.core.interfaces.logging.UserEntryLogger
|
||||
import app.aaps.core.interfaces.profile.Profile
|
||||
import app.aaps.core.interfaces.profile.ProfileFunction
|
||||
import app.aaps.core.interfaces.pump.DetailedBolusInfo
|
||||
import app.aaps.core.interfaces.queue.CommandQueue
|
||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
import app.aaps.database.ValueWrapper
|
||||
import app.aaps.database.entities.EffectiveProfileSwitch
|
||||
import app.aaps.database.entities.GlucoseValue
|
||||
import app.aaps.database.entities.HeartRate
|
||||
import app.aaps.database.entities.OfflineEvent
|
||||
import app.aaps.database.entities.UserEntry
|
||||
import app.aaps.database.entities.ValueWithUnit
|
||||
import app.aaps.database.entities.embedments.InsulinConfiguration
|
||||
import app.aaps.database.impl.AppRepository
|
||||
import app.aaps.database.impl.transactions.CancelCurrentOfflineEventIfAnyTransaction
|
||||
import app.aaps.database.impl.transactions.InsertOrUpdateHeartRateTransaction
|
||||
import app.aaps.shared.tests.TestBase
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
|
@ -27,6 +33,8 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.argThat
|
||||
import org.mockito.ArgumentMatchers.isNull
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.times
|
||||
|
@ -45,13 +53,17 @@ class LoopHubTest: TestBase() {
|
|||
@Mock lateinit var profileFunction: ProfileFunction
|
||||
@Mock lateinit var repo: AppRepository
|
||||
@Mock lateinit var userEntryLogger: UserEntryLogger
|
||||
@Mock lateinit var sp: SP
|
||||
|
||||
private lateinit var loopHub: LoopHubImpl
|
||||
private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC"))
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
loopHub = LoopHubImpl(iobCobCalculator, loop, profileFunction, repo)
|
||||
loopHub = LoopHubImpl(
|
||||
aapsLogger, commandQueue, constraints, iobCobCalculator, loop,
|
||||
profileFunction, repo, userEntryLogger, sp
|
||||
)
|
||||
loopHub.clock = clock
|
||||
}
|
||||
|
||||
|
@ -83,18 +95,10 @@ class LoopHubTest: TestBase() {
|
|||
|
||||
@Test
|
||||
fun testGlucoseUnit() {
|
||||
val profile = mock(Profile::class.java)
|
||||
`when`(profile.units).thenReturn(GlucoseUnit.MMOL)
|
||||
`when`(profileFunction.getProfile()).thenReturn(profile)
|
||||
assertEquals(GlucoseUnit.MMOL, loopHub.glucoseUnit)
|
||||
verify(profileFunction, times(1)).getProfile()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGlucoseUnitNullProfile() {
|
||||
`when`(profileFunction.getProfile()).thenReturn(null)
|
||||
`when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn("mg/dl")
|
||||
assertEquals(GlucoseUnit.MGDL, loopHub.glucoseUnit)
|
||||
verify(profileFunction, times(1)).getProfile()
|
||||
`when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn("mmol")
|
||||
assertEquals(GlucoseUnit.MMOL, loopHub.glucoseUnit)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -165,6 +169,32 @@ class LoopHubTest: TestBase() {
|
|||
verify(loop, times(1)).lastRun
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConnectPump() {
|
||||
val c = mock(Completable::class.java)
|
||||
val dummy = CancelCurrentOfflineEventIfAnyTransaction(0)
|
||||
val matcher = {
|
||||
argThat<CancelCurrentOfflineEventIfAnyTransaction> { t -> t.timestamp == clock.millis() }}
|
||||
`when`(repo.runTransaction(matcher() ?: dummy)).thenReturn(c)
|
||||
loopHub.connectPump()
|
||||
verify(repo).runTransaction(matcher() ?: dummy)
|
||||
verify(commandQueue).cancelTempBasal(true, null)
|
||||
verify(userEntryLogger).log(UserEntry.Action.RECONNECT, UserEntry.Sources.GarminDevice)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDisconnectPump() {
|
||||
val profile = mock(Profile::class.java)
|
||||
`when`(profileFunction.getProfile()).thenReturn(profile)
|
||||
loopHub.disconnectPump(23)
|
||||
verify(profileFunction).getProfile()
|
||||
verify(loop).goToZeroTemp(23, profile, OfflineEvent.Reason.DISCONNECT_PUMP)
|
||||
verify(userEntryLogger).log(
|
||||
UserEntry.Action.DISCONNECT,
|
||||
UserEntry.Sources.GarminDevice,
|
||||
ValueWithUnit.Minute(23))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetGlucoseValues() {
|
||||
val glucoseValues = listOf(
|
||||
|
@ -180,6 +210,25 @@ class LoopHubTest: TestBase() {
|
|||
verify(repo).compatGetBgReadingsDataFromTime(1001_000, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPostCarbs() {
|
||||
@Suppress("unchecked_cast")
|
||||
val constraint = mock(Constraint::class.java) as Constraint<Int>
|
||||
`when`(constraint.value()).thenReturn(99)
|
||||
`when`(constraints.getMaxCarbsAllowed()).thenReturn(constraint)
|
||||
loopHub.postCarbs(100)
|
||||
verify(constraints).getMaxCarbsAllowed()
|
||||
verify(userEntryLogger).log(
|
||||
UserEntry.Action.CARBS,
|
||||
UserEntry.Sources.GarminDevice,
|
||||
ValueWithUnit.Gram(99))
|
||||
verify(commandQueue).bolus(
|
||||
argThat { b ->
|
||||
b!!.eventType == DetailedBolusInfo.EventType.CARBS_CORRECTION &&
|
||||
b.carbs == 99.0 }?: DetailedBolusInfo() ,
|
||||
isNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStoreHeartRate() {
|
||||
val samplingStart = Instant.ofEpochMilli(1_001_000)
|
||||
|
|
Loading…
Reference in a new issue