diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f6683654a..8db9f1ace1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -153,6 +153,7 @@ android { //Deleting it causes a binding error buildFeatures { dataBinding = true + buildConfig = true } } diff --git a/app/src/main/kotlin/app/aaps/MainActivity.kt b/app/src/main/kotlin/app/aaps/MainActivity.kt index b5296ab7c4..bf01cacc20 100644 --- a/app/src/main/kotlin/app/aaps/MainActivity.kt +++ b/app/src/main/kotlin/app/aaps/MainActivity.kt @@ -276,7 +276,7 @@ class MainActivity : DaggerAppCompatActivityWithResult() { }) // Setup views on 2nd and next activity start // On 1st start app is still initializing, start() is delayed and run from EventAppInitialized - if (config.appInitialized) start() + if (config.appInitialized) setupViews() } private fun start() { diff --git a/build.gradle.kts b/build.gradle.kts index 04b3e46ec4..e2bf637c52 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.1.4") + classpath("com.android.tools.build:gradle:8.2.0") classpath("com.google.gms:google-services:4.4.0") classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9") @@ -22,7 +22,7 @@ buildscript { } plugins { - id("org.jlleitschuh.gradle.ktlint") version "11.6.1" + id("org.jlleitschuh.gradle.ktlint") version "12.0.2" } allprojects { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 7cd2f0a647..223da698c9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,7 +1,7 @@ object KtsBuildVersions { - const val gradle = "8.1.3" - const val kotlin = "1.9.0" + const val gradle = "8.2.0" + const val kotlin = "1.9.10" } plugins { diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt index 5a887617e8..ad30c2b047 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt @@ -1,5 +1,6 @@ package app.aaps.core.interfaces.pump +import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.pump.defs.PumpType import app.aaps.core.interfaces.utils.DateUtil @@ -257,6 +258,26 @@ interface PumpSync { **/ fun insertTherapyEventIfNewWithTimestamp(timestamp: Long, type: DetailedBolusInfo.EventType, note: String? = null, pumpId: Long? = null, pumpType: PumpType, pumpSerial: String): Boolean + /** + * Synchronization of FINGER_STICK_BG_VALUE events + * + * Assuming there will be no clash on timestamp from different pumps + * only timestamp and type is compared + * + * If db record doesn't exist, new record is created. + * If exists, data is ignored + * + * @param timestamp timestamp of event from pump history + * @param glucose glucose value + * @param glucoseUnit glucose unit + * @param note note + * @param pumpId pump id from history if available + * @param pumpType pump type like PumpType.ACCU_CHEK_COMBO + * @param pumpSerial pump serial number + * @return true if new record is created + **/ + fun insertFingerBgIfNewWithTimestamp(timestamp: Long, glucose: Double, glucoseUnit: GlucoseUnit, note: String? = null, pumpId: Long? = null, pumpType: PumpType, pumpSerial: String): Boolean + /** * Create an announcement * diff --git a/gradle.properties b/gradle.properties index 743829c702..dc962a673a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -32,5 +32,4 @@ android.nonTransitiveRClass=true # null: KtCallExpression # https://youtrack.jetbrains.com/issue/KT-58027 kapt.use.jvm.ir=false -android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=true diff --git a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt index 1db60cde05..bac571d297 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.pump +import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.UserEntryLogger @@ -16,6 +17,7 @@ import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.T import app.aaps.core.main.events.EventNewNotification +import app.aaps.core.main.extensions.fromConstant import app.aaps.core.main.pump.fromDbPumpType import app.aaps.core.main.pump.toDbPumpType import app.aaps.core.main.pump.toDbSource @@ -291,6 +293,42 @@ class PumpSyncImplementation @Inject constructor( } } + override fun insertFingerBgIfNewWithTimestamp(timestamp: Long, glucose: Double, glucoseUnit: GlucoseUnit, note: String?, pumpId: Long?, pumpType: PumpType, pumpSerial: String): Boolean { + if (!confirmActivePump(timestamp, pumpType, pumpSerial)) return false + var type = TherapyEvent.Type.FINGER_STICK_BG_VALUE + val therapyEvent = TherapyEvent( + timestamp = timestamp, + type = type, + duration = 0, + note = note, + enteredBy = "AndroidAPS", + glucose = glucose, + glucoseType = TherapyEvent.MeterType.FINGER, + glucoseUnit = TherapyEvent.GlucoseUnit.fromConstant(glucoseUnit), + interfaceIDs_backing = InterfaceIDs( + pumpId = pumpId, + pumpType = pumpType.toDbPumpType(), + pumpSerial = pumpSerial + ) + ) + uel.log( + action = UserEntry.Action.CAREPORTAL, + source = pumpType.source.toDbSource(), + note = note, + timestamp = timestamp, + ValueWithUnit.Timestamp(timestamp), ValueWithUnit.TherapyEventType(type) + ) + repository.runTransactionForResult(InsertIfNewByTimestampTherapyEventTransaction(therapyEvent)) + .doOnError { + aapsLogger.error(LTag.DATABASE, "Error while saving TherapyEvent", it) + } + .blockingGet() + .also { result -> + result.inserted.forEach { aapsLogger.debug(LTag.DATABASE, "Inserted TherapyEvent $it") } + return result.inserted.size > 0 + } + } + override fun insertAnnouncement(error: String, pumpId: Long?, pumpType: PumpType, pumpSerial: String) { if (!confirmActivePump(dateUtil.now(), pumpType, pumpSerial)) return disposable += repository.runTransaction(InsertTherapyEventAnnouncementTransaction(error, pumpId, pumpType.toDbPumpType(), pumpSerial)) diff --git a/plugins/aps/src/main/res/values-es-rES/strings.xml b/plugins/aps/src/main/res/values-es-rES/strings.xml index b9b3566744..f46d421cb7 100644 --- a/plugins/aps/src/main/res/values-es-rES/strings.xml +++ b/plugins/aps/src/main/res/values-es-rES/strings.xml @@ -1,7 +1,7 @@ Habilitar la relación de sensibilidad basada en TDD para modificar las basales y el objetivo de glucosa - Utiliza las últimas 24h TDD/7D TDD para calcular el ratio de sensibilidad utilizado para aumentar o disminuir la tasa basal, y también ajustar el objetivo de glucosa si estas opciones están activadas, de la misma forma que lo hace Autosens. Se recomienda comenzar con esta opción desactivada + Utiliza las últimas 24h TDD/7D TDD para calcular el factor de sensibilidad utilizado para aumentar o disminuir la tasa basal, y también ajusta el objetivo de glucosa si estas opciones están activadas, de la misma forma que lo hace Autosens. Se recomienda comenzar con esta opción desactivada Factor de ajuste de ISF Dinámico % Factor de ajuste para ISF Dinámico. Establezca más de 100% para una corrección más agresiva, y menos de 100% para correcciones más susves. Objetivo temporal alto aumenta la sensibilidad diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt index 5eb7f3d41d..0415399cc4 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt @@ -300,7 +300,7 @@ class ProfileFragment : DaggerFragment() { binding.profileRemove.setOnClickListener { activity?.let { activity -> - OKDialog.showConfirmation(activity, rh.gs(R.string.delete_current_profile), { + OKDialog.showConfirmation(activity, rh.gs(R.string.delete_current_profile, profilePlugin.currentProfile()?.name), { uel.log( UserEntry.Action.PROFILE_REMOVED, UserEntry.Sources.LocalProfile, ValueWithUnit.SimpleString( profilePlugin.currentProfile()?.name diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index a46b55b81e..ced0a2cf96 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -149,7 +149,7 @@ add new to list Do you want to switch profile and discard changes made to current profile? Save or reset current changes first - Delete current profile? + Delete profile \"%1$s\"? Units: Missing profile name Error in IC values diff --git a/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml b/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml index f24cdc1d85..36e143d917 100644 --- a/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml @@ -2,9 +2,9 @@ UTJEVNING Eksponentiell utjevning - "Andre ordens algoritme for eksponentiell utjevning" + "Algoritme for eksponentiell utjevning, nyeste BS-verdi påvirkes" Gjennomsnittlig utjevning - "Gjennomsnittlig utjevnings-algoritme, nyeste verdi påvirkes ikke" + "Algoritme for gjennomsnittlig utjevning, nyeste BS-verdi påvirkes ikke. Kan minne om BYODA G6 sin utjevningsalgoritme" Ingen utjevning - "Ingen utjevning utføres på motatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6." + "Ingen utjevning utføres på mottatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6." diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 6981d3d58e..617e84cdc1 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -15,10 +15,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.HttpURLConnection import java.net.SocketAddress import java.net.URI @@ -123,7 +127,12 @@ class GarminPlugin @Inject constructor( setupGarminMessenger() } - fun setupHttpServer() { + private fun setupHttpServer() { + setupHttpServer(Duration.ZERO) + } + + @VisibleForTesting + fun setupHttpServer(wait: Duration) { if (sp.getBoolean("communication_http", false)) { val port = sp.getInt("communication_http_port", 28891) if (server != null && server?.port == port) return @@ -133,6 +142,8 @@ class GarminPlugin @Inject constructor( registerEndpoint("/get", requestHandler(::onGetBloodGlucose)) registerEndpoint("/carbs", requestHandler(::onPostCarbs)) registerEndpoint("/connect", requestHandler(::onConnectPump)) + registerEndpoint("/connect", requestHandler(::onSgv)) + awaitReady(wait) } } else if (server != null) { aapsLogger.info(LTag.GARMIN, "stopping HTTP server") @@ -363,4 +374,62 @@ 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(uri: URI): 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") + } + jo.addProperty("iob", loopHub.insulinOnboard + loopHub.insulinBasalOnboard) + loopHub.temporaryBasal.also { + if (!it.isNaN()) { + val temporaryBasalRateInPercent = (it * 100.0).toInt() + jo.addProperty("tbr", temporaryBasalRateInPercent) + } + } + jo.addProperty("cob", loopHub.carbsOnboard) + } + joa.add(jo) + } + return joa.toString() + } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt index 4b420d21f5..ebd8ac209b 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt @@ -20,6 +20,12 @@ interface LoopHub { /** Returns the remaining bolus insulin on board. */ val insulinOnboard: Double + /** Returns the basal insulin on board. */ + val insulinBasalOnboard: Double + + /** Returns the remaining carbs on board. */ + val carbsOnboard: Double? + /** Returns true if the pump is connected. */ val isConnected: Boolean @@ -48,4 +54,4 @@ interface LoopHub { avgHeartRate: Int, device: String? ) -} \ No newline at end of file +} diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt index dfcf9dab37..1e9f19ae7f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt @@ -13,6 +13,7 @@ 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.core.main.graph.OverviewData import app.aaps.database.ValueWrapper import app.aaps.database.entities.EffectiveProfileSwitch import app.aaps.database.entities.GlucoseValue @@ -42,6 +43,7 @@ class LoopHubImpl @Inject constructor( private val repo: AppRepository, private val userEntryLogger: UserEntryLogger, private val sp: SP, + private val overviewData: OverviewData, ) : LoopHub { @VisibleForTesting @@ -64,6 +66,14 @@ class LoopHubImpl @Inject constructor( override val insulinOnboard: Double get() = iobCobCalculator.calculateIobFromBolus().iob + /** Returns the remaining bolus and basal insulin on board. */ + override val insulinBasalOnboard :Double + get() = iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().basaliob + + /** Returns the remaining carbs on board. */ + override val carbsOnboard: Double? + get() = overviewData.cobInfo(iobCobCalculator).displayCob + /** Returns true if the pump is connected. */ override val isConnected: Boolean get() = !loop.isDisconnected @@ -142,4 +152,4 @@ class LoopHubImpl @Inject constructor( ) repo.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait() } -} \ No newline at end of file +} diff --git a/plugins/sync/src/main/res/values-es-rES/strings.xml b/plugins/sync/src/main/res/values-es-rES/strings.xml index 05ce3e3902..0c777ef050 100644 --- a/plugins/sync/src/main/res/values-es-rES/strings.xml +++ b/plugins/sync/src/main/res/values-es-rES/strings.xml @@ -128,7 +128,7 @@ Supervisar y controlar AAPS usando un reloj WearOS (Ningún reloj conectado) Estado de la bomba de insulina - Estado del lazo + Estado del bucle Calc. Asistente:\nInsulina: %1$.2fU\nCarbohidratos: %2$dg El asistente rápido seleccionado ya no está disponible, por favor actualice su tarjeta Asistente Rápido: %1$s\nInsulina: %2$.2fU\nCarbohidratos: %3$dg diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt index d60ab839d2..8d4d6b2cc2 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -14,13 +14,13 @@ import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest import org.junit.jupiter.api.Test import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.atMost import org.mockito.Mockito.mock @@ -28,15 +28,21 @@ 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.ConnectException import java.net.HttpURLConnection import java.net.SocketAddress import java.net.URI import java.time.Clock +import java.time.Duration 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 @@ -60,13 +66,17 @@ class GarminPluginTest: TestBase() { `when`(sp.getBoolean(anyString(), anyBoolean())).thenAnswer { i -> i.arguments[1] } `when`(sp.getString(anyString(), anyString())).thenAnswer { i -> i.arguments[1] } `when`(sp.getInt(anyString(), anyInt())).thenAnswer { i -> i.arguments[1] } - `when`(sp.getInt(eq("communication_http_port") ?: "", anyInt())) + `when`(sp.getInt(eq("communication_http_port"), anyInt())) .thenReturn(28890) } @AfterEach fun verifyNoFurtherInteractions() { verify(loopHub, atMost(2)).currentProfileName + verify(loopHub, atMost(3)).insulinOnboard + verify(loopHub, atMost(3)).insulinBasalOnboard + verify(loopHub, atMost(3)).temporaryBasal + verify(loopHub, atMost(3)).carbsOnboard verifyNoMoreInteractions(loopHub) } @@ -86,8 +96,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 ) @@ -149,20 +160,20 @@ class GarminPluginTest: TestBase() { fun setupHttpServer_enabled() { `when`(sp.getBoolean("communication_http", false)).thenReturn(true) `when`(sp.getInt("communication_http_port", 28891)).thenReturn(28892) - gp.setupHttpServer() + gp.setupHttpServer(Duration.ofSeconds(10)) val reqUri = URI("http://127.0.0.1:28892/get") val resp = reqUri.toURL().openConnection() as HttpURLConnection assertEquals(200, resp.responseCode) // Change port `when`(sp.getInt("communication_http_port", 28891)).thenReturn(28893) - gp.setupHttpServer() + gp.setupHttpServer(Duration.ofSeconds(10)) val reqUri2 = URI("http://127.0.0.1:28893/get") val resp2 = reqUri2.toURL().openConnection() as HttpURLConnection assertEquals(200, resp2.responseCode) `when`(sp.getBoolean("communication_http", false)).thenReturn(false) - gp.setupHttpServer() + gp.setupHttpServer(Duration.ofSeconds(10)) assertThrows(ConnectException::class.java) { (reqUri2.toURL().openConnection() as HttpURLConnection).responseCode } @@ -177,7 +188,7 @@ class GarminPluginTest: TestBase() { @Test fun setupHttpServer_disabled() { - gp.setupHttpServer() + gp.setupHttpServer(Duration.ofSeconds(10)) val reqUri = URI("http://127.0.0.1:28890/get") assertThrows(ConnectException::class.java) { (reqUri.toURL().openConnection() as HttpURLConnection).responseCode @@ -252,7 +263,7 @@ class GarminPluginTest: TestBase() { gp.onConnectDevice(device) val captor = ArgumentCaptor.forClass(Any::class.java) - verify(gp.garminMessenger)!!.sendMessage(eq(device) ?: device, captor.capture() ?: "") + verify(gp.garminMessenger)!!.sendMessage(eq(device), captor.capture() ?: "") @Suppress("UNCHECKED_CAST") val r = captor.value as Map assertEquals("foo", r["key"]) @@ -357,4 +368,65 @@ 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(createUri(mapOf()))) + verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false) + } + + @Test +fun onSgv_NoDelta() { + whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL) + whenever(loopHub.insulinOnboard).thenReturn(2.7) + whenever(loopHub.insulinBasalOnboard).thenReturn(2.5) + whenever(loopHub.temporaryBasal).thenReturn(0.8) + whenever(loopHub.carbsOnboard).thenReturn(10.7) + 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","iob":5.2,"tbr":80,"cob":10.7}]""", + gp.onSgv(createUri(mapOf()))) + verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false) + verify(loopHub).glucoseUnit + } + + @Test + fun onSgv() { + whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL) + whenever(loopHub.insulinOnboard).thenReturn(2.7) + whenever(loopHub.insulinBasalOnboard).thenReturn(2.5) + whenever(loopHub.temporaryBasal).thenReturn(0.8) + whenever(loopHub.carbsOnboard).thenReturn(10.7) + whenever(loopHub.getGlucoseValues(any(), eq(false))).thenAnswer { i -> + val from = i.getArgument(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","iob":5.2,"tbr":80,"cob":10.7}]""", + gp.onSgv(createUri(mapOf("count" to "1")))) + 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","iob":5.2,"tbr":80,"cob":10.7},""" + + """{"_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(createUri(mapOf("count" to "2")))) + verify(loopHub).getGlucoseValues( + clock.instant().minusSeconds(900L), false) + + assertEquals( + """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7},""" + + """{"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", + gp.onSgv(createUri(mapOf("count" to "2", "brief_mode" to "true")))) + verify(loopHub, times(2)).getGlucoseValues( + clock.instant().minusSeconds(900L), false) + + verify(loopHub, atLeastOnce()).glucoseUnit + } } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt index a88facfa05..c96bcaee4b 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt @@ -5,6 +5,7 @@ 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.CobInfo import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.iob.IobTotal import app.aaps.core.interfaces.logging.UserEntryLogger @@ -13,6 +14,7 @@ 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.core.main.graph.OverviewData import app.aaps.database.ValueWrapper import app.aaps.database.entities.EffectiveProfileSwitch import app.aaps.database.entities.GlucoseValue @@ -54,6 +56,7 @@ class LoopHubTest: TestBase() { @Mock lateinit var repo: AppRepository @Mock lateinit var userEntryLogger: UserEntryLogger @Mock lateinit var sp: SP + @Mock lateinit var overviewData: OverviewData private lateinit var loopHub: LoopHubImpl private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) @@ -62,7 +65,7 @@ class LoopHubTest: TestBase() { fun setup() { loopHub = LoopHubImpl( aapsLogger, commandQueue, constraints, iobCobCalculator, loop, - profileFunction, repo, userEntryLogger, sp + profileFunction, repo, userEntryLogger, sp, overviewData ) loopHub.clock = clock } @@ -76,9 +79,10 @@ class LoopHubTest: TestBase() { verifyNoMoreInteractions(profileFunction) verifyNoMoreInteractions(repo) verifyNoMoreInteractions(userEntryLogger) + verifyNoMoreInteractions(overviewData) } - @Test +@Test fun testCurrentProfile() { val profile = mock(Profile::class.java) `when`(profileFunction.getProfile()).thenReturn(profile) @@ -109,6 +113,22 @@ class LoopHubTest: TestBase() { verify(iobCobCalculator, times(1)).calculateIobFromBolus() } + @Test + fun testBasalOnBoard() { + val iobBasal = IobTotal(time = 0).apply { basaliob = 23.9 } + `when`(iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended()).thenReturn(iobBasal) + assertEquals(23.9, loopHub.insulinBasalOnboard, 1e-10) + verify(iobCobCalculator, times(1)).calculateIobFromTempBasalsIncludingConvertedExtended() + } + + @Test + fun testCarbsOnBoard() { + val cobInfo = CobInfo(0, 12.0, 0.0) + `when`(overviewData.cobInfo(iobCobCalculator)).thenReturn(cobInfo) + assertEquals(12.0, loopHub.carbsOnboard) + verify(overviewData, times(1)).cobInfo(iobCobCalculator) + } + @Test fun testIsConnected() { `when`(loop.isDisconnected).thenReturn(false) @@ -247,4 +267,4 @@ class LoopHubTest: TestBase() { samplingStart, samplingEnd, 101, "Test Device") verify(repo).runTransaction(InsertOrUpdateHeartRateTransaction(hr)) } -} \ No newline at end of file +} diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt index 7ea2c36e19..606d1058e3 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt @@ -153,7 +153,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor( PumpHistoryEntryType.ClearAlarm, PumpHistoryEntryType.ChangeAlarmNotifyMode, PumpHistoryEntryType.EnableDisableRemote, - PumpHistoryEntryType.BGReceived, PumpHistoryEntryType.SensorAlert, PumpHistoryEntryType.ChangeTimeFormat, PumpHistoryEntryType.ChangeReservoirWarningTime, @@ -188,7 +187,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor( PumpHistoryEntryType.ChangeWatchdogEnable, PumpHistoryEntryType.ChangeOtherDeviceID, PumpHistoryEntryType.ReadOtherDevicesIDs, - PumpHistoryEntryType.BGReceived512, PumpHistoryEntryType.SensorStatus, PumpHistoryEntryType.ReadCaptureEventEnabled, PumpHistoryEntryType.ChangeCaptureEventEnable, @@ -206,6 +204,12 @@ class MedtronicPumpHistoryDecoder @Inject constructor( PumpHistoryEntryType.UnabsorbedInsulin, PumpHistoryEntryType.UnabsorbedInsulin512 -> RecordDecodeStatus.Ignored + PumpHistoryEntryType.BGReceived, + PumpHistoryEntryType.BGReceived512 -> { + decodeBgReceived(entry) + RecordDecodeStatus.OK + } + PumpHistoryEntryType.DailyTotals522, PumpHistoryEntryType.DailyTotals523, PumpHistoryEntryType.DailyTotals515, @@ -297,7 +301,9 @@ class MedtronicPumpHistoryDecoder @Inject constructor( } private fun decodeBatteryActivity(entry: PumpHistoryEntry) { - entry.displayableValue = if (entry.head[0] == 0.toByte()) "Battery Removed" else "Battery Replaced" + val isRemoved = entry.head[0] == 0.toByte() + entry.addDecodedData("isRemoved", isRemoved) + entry.displayableValue = if (isRemoved) "Battery Removed" else "Battery Replaced" } private fun decodeBasalProfileStart(entry: PumpHistoryEntry): RecordDecodeStatus { @@ -407,8 +413,11 @@ class MedtronicPumpHistoryDecoder @Inject constructor( } private fun decodeBgReceived(entry: PumpHistoryEntry) { - entry.addDecodedData("amount", (ByteUtil.asUINT8(entry.getRawDataByIndex(0)) shl 3) + (ByteUtil.asUINT8(entry.getRawDataByIndex(3)) shr 5)) - entry.addDecodedData("meter", ByteUtil.substring(entry.rawData, 6, 3)) // index moved from 1 -> 0 + val glucoseMgdl = (ByteUtil.asUINT8(entry.head[0]) shl 3) + (ByteUtil.asUINT8(entry.datetime[2]) shr 5) + val meterSerial = ByteUtil.shortHexStringWithoutSpaces(entry.body) + entry.addDecodedData("GlucoseMgdl", glucoseMgdl) + entry.addDecodedData("MeterSerial", meterSerial) + entry.displayableValue = String.format("Glucose: %d mg/dl, Meter Serial: %s", glucoseMgdl, meterSerial) } private fun decodeCalBGForPH(entry: PumpHistoryEntry) { diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt index 27d052bcca..03bf6c49c3 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt @@ -4,6 +4,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.Notification import app.aaps.core.interfaces.plugin.ActivePlugin +import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.defs.PumpType @@ -67,7 +68,8 @@ class MedtronicHistoryData @Inject constructor( val medtronicPumpStatus: MedtronicPumpStatus, private val pumpSync: PumpSync, private val pumpSyncStorage: PumpSyncStorage, - private val uiInteraction: UiInteraction + private val uiInteraction: UiInteraction, + private val profileUtil: ProfileUtil ) { val allHistory: MutableList = mutableListOf() @@ -322,6 +324,17 @@ class MedtronicHistoryData @Inject constructor( * Process History Data: Boluses(Treatments), TDD, TBRs, Suspend-Resume (or other pump stops: battery, prime) */ fun processNewHistoryData() { + // Finger BG (for adding entry to careportal) + val bgRecords: MutableList = getFilteredItems(setOf(PumpHistoryEntryType.BGReceived, PumpHistoryEntryType.BGReceived512)) + aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: BGReceived [count=%d, items=%s]", bgRecords.size, gson.toJson(bgRecords))) + if (isCollectionNotEmpty(bgRecords)) { + try { + processBgReceived(bgRecords) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, "ProcessHistoryData: Error processing BGReceived entries: " + ex.message, ex) + throw ex + } + } // Prime (for resetting autosense) val primeRecords: MutableList = getFilteredItems(PumpHistoryEntryType.Prime) @@ -347,6 +360,18 @@ class MedtronicHistoryData @Inject constructor( } } + // BatteryChange + val batteryChangeRecords: MutableList = getFilteredItems(PumpHistoryEntryType.BatteryChange) + aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: BatteryChange [count=%d, items=%s]", batteryChangeRecords.size, gson.toJson(batteryChangeRecords))) + if (isCollectionNotEmpty(batteryChangeRecords)) { + try { + processBatteryChange(batteryChangeRecords) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, "ProcessHistoryData: Error processing BatteryChange entries: " + ex.message, ex) + throw ex + } + } + // TDD val tdds: MutableList = getFilteredItems(setOf(PumpHistoryEntryType.EndResultTotals, getTDDType())) aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: TDD [count=%d, items=%s]", tdds.size, gson.toJson(tdds))) @@ -407,6 +432,34 @@ class MedtronicHistoryData @Inject constructor( } } + fun processBgReceived(bgRecords: List) { + for (bgRecord in bgRecords) { + val glucoseMgdl = bgRecord.getDecodedDataEntry("GlucoseMgdl") + if (glucoseMgdl == null || glucoseMgdl as Int == 0) { + continue + } + + val glucose = profileUtil.fromMgdlToUnits(glucoseMgdl.toDouble()) + val glucoseUnit = profileUtil.units + + val result = pumpSync.insertFingerBgIfNewWithTimestamp( + DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime), + glucose, glucoseUnit, null, + bgRecord.pumpId, + medtronicPumpStatus.pumpType, + medtronicPumpStatus.serialNumber + ) + + aapsLogger.debug( + LTag.PUMP, String.format( + Locale.ROOT, "insertFingerBgIfNewWithTimestamp [date=%d, glucose=%f, glucoseUnit=%s, pumpId=%d, pumpSerial=%s] - Result: %b", + bgRecord.atechDateTime, glucose, glucoseUnit, bgRecord.pumpId, + medtronicPumpStatus.serialNumber, result + ) + ) + } + } + private fun processPrime(primeRecords: List) { val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -30) var lastPrimeRecordTime = 0L @@ -456,6 +509,35 @@ class MedtronicHistoryData @Inject constructor( } } + private fun processBatteryChange(batteryChangeRecords: List) { + val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -120) + var lastBatteryChangeRecordTime = 0L + var lastBatteryChangeRecord: PumpHistoryEntry? = null + for (batteryChangeRecord in batteryChangeRecords) { + val isRemoved = batteryChangeRecord.getDecodedDataEntry("isRemoved") + + if (isRemoved != null && isRemoved as Boolean) + { + // we're interested in battery replacements, not battery removals + continue + } + + if (batteryChangeRecord.atechDateTime > maxAllowedTimeInPast) { + if (lastBatteryChangeRecordTime < batteryChangeRecord.atechDateTime) { + lastBatteryChangeRecordTime = batteryChangeRecord.atechDateTime + lastBatteryChangeRecord = batteryChangeRecord + } + } + } + if (lastBatteryChangeRecord != null) { + uploadCareportalEventIfFoundInHistory( + lastBatteryChangeRecord, + MedtronicConst.Statistics.LastBatteryChange, + DetailedBolusInfo.EventType.PUMP_BATTERY_CHANGE + ) + } + } + private fun uploadCareportalEventIfFoundInHistory(historyRecord: PumpHistoryEntry, eventSP: String, eventType: DetailedBolusInfo.EventType) { val lastPrimeFromAAPS = sp.getLong(eventSP, 0L) if (historyRecord.atechDateTime != lastPrimeFromAAPS) { diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt index 8d86bce696..03bc600aa6 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt @@ -30,5 +30,6 @@ object MedtronicConst { const val LastPumpHistoryEntry = StatsPrefix + "pump_history_entry" const val LastPrime = StatsPrefix + "last_sent_prime" const val LastRewind = StatsPrefix + "last_sent_rewind" + const val LastBatteryChange = StatsPrefix + "last_sent_battery_change" } } \ No newline at end of file diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt index 03cfe9d431..0f25f4cd98 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt @@ -4,7 +4,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.sharedPreferences.SP -import app.aaps.shared.tests.TestBase +import app.aaps.shared.tests.TestBaseWithProfile import dagger.android.AndroidInjector import dagger.android.HasAndroidInjector import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil @@ -16,15 +16,13 @@ import info.nightscout.pump.common.sync.PumpSyncStorage import org.mockito.Answers import org.mockito.Mock -open class MedtronicTestBase : TestBase() { +open class MedtronicTestBase : TestBaseWithProfile() { var rileyLinkUtil = RileyLinkUtil() @Mock lateinit var pumpSync: PumpSync @Mock lateinit var pumpSyncStorage: PumpSyncStorage - @Mock(answer = Answers.RETURNS_DEEP_STUBS) lateinit var activePlugin: ActivePlugin - @Mock lateinit var sp: SP - @Mock lateinit var rh: ResourceHelper + @Mock(answer = Answers.RETURNS_DEEP_STUBS) override lateinit var activePlugin: ActivePlugin lateinit var medtronicUtil: MedtronicUtil lateinit var decoder: MedtronicPumpHistoryDecoder @@ -53,6 +51,24 @@ open class MedtronicTestBase : TestBase() { } + fun getPumpHistoryEntryFromData(vararg elements: Int): PumpHistoryEntry { + val data: MutableList = ArrayList() + for (item in elements) { + var b = if (item > 128) item - 256 else item + data.add(b.toByte()); + } + + val entryType = PumpHistoryEntryType.getByCode(data[0]) + + val phe = PumpHistoryEntry() + phe.setEntryType(medtronicUtil.medtronicPumpModel, entryType) + phe.setData(data, false) + + decoder.decodeRecord(phe) + + return phe + } + private fun preProcessTBRs(tbrsInput: MutableList): MutableList { val tbrs: MutableList = mutableListOf() val map: MutableMap = HashMap() diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt index 70bcd924e4..13a53c78f7 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt @@ -40,7 +40,7 @@ import org.mockito.Mock decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) medtronicHistoryData = MedtronicHistoryData( packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, - medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction + medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil ) diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt index 8b07c7dc2c..2743439726 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt @@ -1,8 +1,16 @@ package info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump +import app.aaps.core.interfaces.ui.UiInteraction import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase +import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.MedtronicPumpHistoryDecoder +import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicDeviceType +import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus +import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.Mockito.`when` /** * Created by andy on 4/9/19. @@ -10,6 +18,16 @@ import org.junit.jupiter.api.Test */ class PumpHistoryEntryUTest : MedtronicTestBase() { + @Mock lateinit var medtronicPumpStatus: MedtronicPumpStatus + @Mock lateinit var uiInteraction: UiInteraction + + @BeforeEach + fun setUp() { + medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction) + `when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel) + decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) + } + @Test fun checkIsAfter() { val dateObject = 20191010000000L @@ -18,4 +36,21 @@ class PumpHistoryEntryUTest : MedtronicTestBase() { phe.atechDateTime = dateObject assertThat(phe.isAfter(queryObject)).isTrue() } + + @Test + fun decodeBgReceived() { + val bgRecord = getPumpHistoryEntryFromData( + // head + 0x39, 0x15, + // datetime (combined with glucose in mg/dl) + 0xC2, 0x25, 0xF3, 0x61, 0x17, + // serial number + 0x12, 0x34, 0x56 + ) + val expectedGlucoseMgdl = 175 + val expectedMeterSerial = "123456" + + assertThat(bgRecord.getDecodedDataEntry("GlucoseMgdl")).isEqualTo(expectedGlucoseMgdl) + assertThat(bgRecord.getDecodedDataEntry("MeterSerial")).isEqualTo(expectedMeterSerial) + } } diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt index d36c40d1ba..2a91afaba4 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt @@ -1,18 +1,24 @@ package info.nightscout.androidaps.plugins.pump.medtronic.data +import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.ui.UiInteraction +import app.aaps.core.utils.DateTimeUtil import com.google.gson.Gson import com.google.gson.internal.LinkedTreeMap import com.google.gson.reflect.TypeToken import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.MedtronicPumpHistoryDecoder import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntry +import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntryType import info.nightscout.androidaps.plugins.pump.medtronic.data.dto.TempBasalPair +import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicDeviceType import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import java.lang.reflect.Type @Suppress("UNCHECKED_CAST") @@ -24,6 +30,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() { @BeforeEach fun setUp() { medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction) + `when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel) decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) } @@ -32,7 +39,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() { val unitToTest = MedtronicHistoryData( packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, - medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction + medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil ) val gson = Gson() @@ -75,7 +82,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() { medtronicUtil, decoder, medtronicPumpStatus, pumpSync, - pumpSyncStorage, uiInteraction + pumpSyncStorage, uiInteraction, profileUtil ) val gson = Gson() @@ -110,4 +117,70 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() { } + @Test + fun processBgReceived_WithMgdl() { + + val unitToTest = MedtronicHistoryData( + packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, + medtronicUtil, decoder, + medtronicPumpStatus, + pumpSync, + pumpSyncStorage, uiInteraction, profileUtil + ) + + val glucoseMgdl = 175 + + `when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn(GlucoseUnit.MGDL.asText) + + val bgRecord = PumpHistoryEntry() + bgRecord.setEntryType(medtronicUtil.medtronicPumpModel, PumpHistoryEntryType.BGReceived) + bgRecord.addDecodedData("GlucoseMgdl", glucoseMgdl) + bgRecord.addDecodedData("MeterSerial", "123456") + + unitToTest.processBgReceived(listOf(bgRecord)) + + verify(pumpSync).insertFingerBgIfNewWithTimestamp( + DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime), + glucoseMgdl.toDouble(), + GlucoseUnit.MGDL, null, + bgRecord.pumpId, + medtronicPumpStatus.pumpType, + medtronicPumpStatus.serialNumber + ) + + } + + @Test + fun processBgReceived_WithMmol() { + + val unitToTest = MedtronicHistoryData( + packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, + medtronicUtil, decoder, + medtronicPumpStatus, + pumpSync, + pumpSyncStorage, uiInteraction, profileUtil + ) + val glucoseMgdl = 180 + val glucoseMmol = 10.0 + + `when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn(GlucoseUnit.MMOL.asText) + + val bgRecord = PumpHistoryEntry() + bgRecord.setEntryType(medtronicUtil.medtronicPumpModel, PumpHistoryEntryType.BGReceived) + bgRecord.addDecodedData("GlucoseMgdl", glucoseMgdl) + bgRecord.addDecodedData("MeterSerial", "123456") + + unitToTest.processBgReceived(listOf(bgRecord)) + + verify(pumpSync).insertFingerBgIfNewWithTimestamp( + DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime), + glucoseMmol, + GlucoseUnit.MMOL, null, + bgRecord.pumpId, + medtronicPumpStatus.pumpType, + medtronicPumpStatus.serialNumber + ) + + } + } diff --git a/pump/omnipod-common/src/main/res/values/strings.xml b/pump/omnipod-common/src/main/res/values/strings.xml index d2ecea3ce0..0a2757f7c5 100644 --- a/pump/omnipod-common/src/main/res/values/strings.xml +++ b/pump/omnipod-common/src/main/res/values/strings.xml @@ -158,6 +158,7 @@ Activation time exceeded Inactive Pod Fault: %1$03d %2$s + Normal Deactivate Pod diff --git a/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 19387cc705..a0acf389de 100644 --- a/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -991,9 +991,9 @@ class OmnipodDashPumpPlugin @Inject constructor( val extended = JSONObject() try { val podStatus = when { - podStateManager.isPodRunning && podStateManager.isSuspended -> "suspended" - podStateManager.isPodRunning -> "normal" - else -> "no active Pod" + podStateManager.isPodRunning && podStateManager.isSuspended -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_suspended).lowercase() + podStateManager.isPodRunning -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_normal).lowercase() + else -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod).lowercase() } status.put("status", podStatus) status.put("timestamp", dateUtil.toISOString(podStateManager.lastUpdatedSystem)) diff --git a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt index 3a170fb5f2..42997cc0ca 100644 --- a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt +++ b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt @@ -36,7 +36,7 @@ import org.mockito.invocation.InvocationOnMock @Suppress("SpellCheckingInspection") open class TestBaseWithProfile : TestBase() { - @Mock lateinit var activePlugin: ActivePlugin + @Mock open lateinit var activePlugin: ActivePlugin @Mock lateinit var rh: ResourceHelper @Mock lateinit var iobCobCalculator: IobCobCalculator @Mock lateinit var fabricPrivacy: FabricPrivacy diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index ad13b8e525..ce0b3e4539 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -86,6 +86,9 @@ android { versionName = Versions.appVersion + "-aapsclient2" } } + buildFeatures { + buildConfig = true + } } allprojects {