pull upstream/dev

This commit is contained in:
Robert Buessow 2023-12-05 15:36:05 +01:00
commit f0e7134d29
28 changed files with 501 additions and 45 deletions

View file

@ -153,6 +153,7 @@ android {
//Deleting it causes a binding error //Deleting it causes a binding error
buildFeatures { buildFeatures {
dataBinding = true dataBinding = true
buildConfig = true
} }
} }

View file

@ -276,7 +276,7 @@ class MainActivity : DaggerAppCompatActivityWithResult() {
}) })
// Setup views on 2nd and next activity start // Setup views on 2nd and next activity start
// On 1st start app is still initializing, start() is delayed and run from EventAppInitialized // 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() { private fun start() {

View file

@ -8,7 +8,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { 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.gms:google-services:4.4.0")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9") classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9")
@ -22,7 +22,7 @@ buildscript {
} }
plugins { plugins {
id("org.jlleitschuh.gradle.ktlint") version "11.6.1" id("org.jlleitschuh.gradle.ktlint") version "12.0.2"
} }
allprojects { allprojects {

View file

@ -1,7 +1,7 @@
object KtsBuildVersions { object KtsBuildVersions {
const val gradle = "8.1.3" const val gradle = "8.2.0"
const val kotlin = "1.9.0" const val kotlin = "1.9.10"
} }
plugins { plugins {

View file

@ -1,5 +1,6 @@
package app.aaps.core.interfaces.pump 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.profile.Profile
import app.aaps.core.interfaces.pump.defs.PumpType import app.aaps.core.interfaces.pump.defs.PumpType
import app.aaps.core.interfaces.utils.DateUtil 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 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 * Create an announcement
* *

View file

@ -32,5 +32,4 @@ android.nonTransitiveRClass=true
# null: KtCallExpression # null: KtCallExpression
# https://youtrack.jetbrains.com/issue/KT-58027 # https://youtrack.jetbrains.com/issue/KT-58027
kapt.use.jvm.ir=false kapt.use.jvm.ir=false
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=true android.nonFinalResIds=true

View file

@ -1,5 +1,6 @@
package app.aaps.implementation.pump 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.AAPSLogger
import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.logging.UserEntryLogger 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.DateUtil
import app.aaps.core.interfaces.utils.T import app.aaps.core.interfaces.utils.T
import app.aaps.core.main.events.EventNewNotification 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.fromDbPumpType
import app.aaps.core.main.pump.toDbPumpType import app.aaps.core.main.pump.toDbPumpType
import app.aaps.core.main.pump.toDbSource 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) { override fun insertAnnouncement(error: String, pumpId: Long?, pumpType: PumpType, pumpSerial: String) {
if (!confirmActivePump(dateUtil.now(), pumpType, pumpSerial)) return if (!confirmActivePump(dateUtil.now(), pumpType, pumpSerial)) return
disposable += repository.runTransaction(InsertTherapyEventAnnouncementTransaction(error, pumpId, pumpType.toDbPumpType(), pumpSerial)) disposable += repository.runTransaction(InsertTherapyEventAnnouncementTransaction(error, pumpId, pumpType.toDbPumpType(), pumpSerial))

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="dynisf_adjust_sensitivity">Habilitar la relación de sensibilidad basada en TDD para modificar las basales y el objetivo de glucosa</string> <string name="dynisf_adjust_sensitivity">Habilitar la relación de sensibilidad basada en TDD para modificar las basales y el objetivo de glucosa</string>
<string name="dynisf_adjust_sensitivity_summary">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</string> <string name="dynisf_adjust_sensitivity_summary">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</string>
<string name="DynISFAdjust_title" formatted="false">Factor de ajuste de ISF Dinámico %</string> <string name="DynISFAdjust_title" formatted="false">Factor de ajuste de ISF Dinámico %</string>
<string name="DynISFAdjust_summary" formatted="false">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.</string> <string name="DynISFAdjust_summary" formatted="false">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.</string>
<string name="high_temptarget_raises_sensitivity_title">Objetivo temporal alto aumenta la sensibilidad</string> <string name="high_temptarget_raises_sensitivity_title">Objetivo temporal alto aumenta la sensibilidad</string>

View file

@ -300,7 +300,7 @@ class ProfileFragment : DaggerFragment() {
binding.profileRemove.setOnClickListener { binding.profileRemove.setOnClickListener {
activity?.let { activity -> 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( uel.log(
UserEntry.Action.PROFILE_REMOVED, UserEntry.Sources.LocalProfile, ValueWithUnit.SimpleString( UserEntry.Action.PROFILE_REMOVED, UserEntry.Sources.LocalProfile, ValueWithUnit.SimpleString(
profilePlugin.currentProfile()?.name profilePlugin.currentProfile()?.name

View file

@ -149,7 +149,7 @@
<string name="a11y_add_new_to_list">add new to list</string> <string name="a11y_add_new_to_list">add new to list</string>
<string name="do_you_want_switch_profile">Do you want to switch profile and discard changes made to current profile?</string> <string name="do_you_want_switch_profile">Do you want to switch profile and discard changes made to current profile?</string>
<string name="save_or_reset_changes_first">Save or reset current changes first</string> <string name="save_or_reset_changes_first">Save or reset current changes first</string>
<string name="delete_current_profile">Delete current profile?</string> <string name="delete_current_profile">Delete profile \"%1$s\"?</string>
<string name="units_colon">Units:</string> <string name="units_colon">Units:</string>
<string name="missing_profile_name">Missing profile name</string> <string name="missing_profile_name">Missing profile name</string>
<string name="error_in_ic_values">Error in IC values</string> <string name="error_in_ic_values">Error in IC values</string>

View file

@ -2,9 +2,9 @@
<resources> <resources>
<string name="smoothing_shortname">UTJEVNING</string> <string name="smoothing_shortname">UTJEVNING</string>
<string name="exponential_smoothing_name">Eksponentiell utjevning</string> <string name="exponential_smoothing_name">Eksponentiell utjevning</string>
<string name="description_exponential_smoothing">"Andre ordens algoritme for eksponentiell utjevning"</string> <string name="description_exponential_smoothing">"Algoritme for eksponentiell utjevning, nyeste BS-verdi påvirkes"</string>
<string name="avg_smoothing_name">Gjennomsnittlig utjevning</string> <string name="avg_smoothing_name">Gjennomsnittlig utjevning</string>
<string name="description_avg_smoothing">"Gjennomsnittlig utjevnings-algoritme, nyeste verdi påvirkes ikke"</string> <string name="description_avg_smoothing">"Algoritme for gjennomsnittlig utjevning, nyeste BS-verdi påvirkes ikke. Kan minne om BYODA G6 sin utjevningsalgoritme"</string>
<string name="no_smoothing_name">Ingen utjevning</string> <string name="no_smoothing_name">Ingen utjevning</string>
<string name="description_no_smoothing">"Ingen utjevning utføres på motatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6."</string> <string name="description_no_smoothing">"Ingen utjevning utføres på mottatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6."</string>
</resources> </resources>

View file

@ -15,10 +15,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.HttpURLConnection import java.net.HttpURLConnection
import java.net.SocketAddress import java.net.SocketAddress
import java.net.URI import java.net.URI
@ -123,7 +127,12 @@ class GarminPlugin @Inject constructor(
setupGarminMessenger() setupGarminMessenger()
} }
fun setupHttpServer() { private fun setupHttpServer() {
setupHttpServer(Duration.ZERO)
}
@VisibleForTesting
fun setupHttpServer(wait: Duration) {
if (sp.getBoolean("communication_http", false)) { if (sp.getBoolean("communication_http", false)) {
val port = sp.getInt("communication_http_port", 28891) val port = sp.getInt("communication_http_port", 28891)
if (server != null && server?.port == port) return if (server != null && server?.port == port) return
@ -133,6 +142,8 @@ class GarminPlugin @Inject constructor(
registerEndpoint("/get", requestHandler(::onGetBloodGlucose)) registerEndpoint("/get", requestHandler(::onGetBloodGlucose))
registerEndpoint("/carbs", requestHandler(::onPostCarbs)) registerEndpoint("/carbs", requestHandler(::onPostCarbs))
registerEndpoint("/connect", requestHandler(::onConnectPump)) registerEndpoint("/connect", requestHandler(::onConnectPump))
registerEndpoint("/connect", requestHandler(::onSgv))
awaitReady(wait)
} }
} else if (server != null) { } else if (server != null) {
aapsLogger.info(LTag.GARMIN, "stopping HTTP server") aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
@ -363,4 +374,62 @@ 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(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()
}
} }

View file

@ -20,6 +20,12 @@ interface LoopHub {
/** Returns the remaining bolus insulin on board. */ /** Returns the remaining bolus insulin on board. */
val insulinOnboard: Double 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. */ /** Returns true if the pump is connected. */
val isConnected: Boolean val isConnected: Boolean
@ -48,4 +54,4 @@ interface LoopHub {
avgHeartRate: Int, avgHeartRate: Int,
device: String? device: String?
) )
} }

View file

@ -13,6 +13,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction
import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.pump.DetailedBolusInfo
import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.queue.CommandQueue
import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.core.main.graph.OverviewData
import app.aaps.database.ValueWrapper import app.aaps.database.ValueWrapper
import app.aaps.database.entities.EffectiveProfileSwitch import app.aaps.database.entities.EffectiveProfileSwitch
import app.aaps.database.entities.GlucoseValue import app.aaps.database.entities.GlucoseValue
@ -42,6 +43,7 @@ class LoopHubImpl @Inject constructor(
private val repo: AppRepository, private val repo: AppRepository,
private val userEntryLogger: UserEntryLogger, private val userEntryLogger: UserEntryLogger,
private val sp: SP, private val sp: SP,
private val overviewData: OverviewData,
) : LoopHub { ) : LoopHub {
@VisibleForTesting @VisibleForTesting
@ -64,6 +66,14 @@ class LoopHubImpl @Inject constructor(
override val insulinOnboard: Double override val insulinOnboard: Double
get() = iobCobCalculator.calculateIobFromBolus().iob 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. */ /** Returns true if the pump is connected. */
override val isConnected: Boolean get() = !loop.isDisconnected override val isConnected: Boolean get() = !loop.isDisconnected
@ -142,4 +152,4 @@ class LoopHubImpl @Inject constructor(
) )
repo.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait() repo.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait()
} }
} }

View file

@ -128,7 +128,7 @@
<string name="description_wear">Supervisar y controlar AAPS usando un reloj WearOS</string> <string name="description_wear">Supervisar y controlar AAPS usando un reloj WearOS</string>
<string name="no_watch_connected">(Ningún reloj conectado)</string> <string name="no_watch_connected">(Ningún reloj conectado)</string>
<string name="pump_status">Estado de la bomba de insulina</string> <string name="pump_status">Estado de la bomba de insulina</string>
<string name="loop_status">Estado del lazo</string> <string name="loop_status">Estado del bucle</string>
<string name="wizard_result">Calc. Asistente:\nInsulina: %1$.2fU\nCarbohidratos: %2$dg</string> <string name="wizard_result">Calc. Asistente:\nInsulina: %1$.2fU\nCarbohidratos: %2$dg</string>
<string name="quick_wizard_not_available">El asistente rápido seleccionado ya no está disponible, por favor actualice su tarjeta</string> <string name="quick_wizard_not_available">El asistente rápido seleccionado ya no está disponible, por favor actualice su tarjeta</string>
<string name="quick_wizard_message">Asistente Rápido: %1$s\nInsulina: %2$.2fU\nCarbohidratos: %3$dg</string> <string name="quick_wizard_message">Asistente Rápido: %1$s\nInsulina: %2$.2fU\nCarbohidratos: %3$dg</string>

View file

@ -14,13 +14,13 @@ import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.atMost import org.mockito.Mockito.atMost
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
@ -28,15 +28,21 @@ 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.ConnectException import java.net.ConnectException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.SocketAddress import java.net.SocketAddress
import java.net.URI import java.net.URI
import java.time.Clock import java.time.Clock
import java.time.Duration
import java.time.Instant 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
@ -60,13 +66,17 @@ class GarminPluginTest: TestBase() {
`when`(sp.getBoolean(anyString(), anyBoolean())).thenAnswer { i -> i.arguments[1] } `when`(sp.getBoolean(anyString(), anyBoolean())).thenAnswer { i -> i.arguments[1] }
`when`(sp.getString(anyString(), anyString())).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(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) .thenReturn(28890)
} }
@AfterEach @AfterEach
fun verifyNoFurtherInteractions() { fun verifyNoFurtherInteractions() {
verify(loopHub, atMost(2)).currentProfileName 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) verifyNoMoreInteractions(loopHub)
} }
@ -86,8 +96,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
) )
@ -149,20 +160,20 @@ class GarminPluginTest: TestBase() {
fun setupHttpServer_enabled() { fun setupHttpServer_enabled() {
`when`(sp.getBoolean("communication_http", false)).thenReturn(true) `when`(sp.getBoolean("communication_http", false)).thenReturn(true)
`when`(sp.getInt("communication_http_port", 28891)).thenReturn(28892) `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 reqUri = URI("http://127.0.0.1:28892/get")
val resp = reqUri.toURL().openConnection() as HttpURLConnection val resp = reqUri.toURL().openConnection() as HttpURLConnection
assertEquals(200, resp.responseCode) assertEquals(200, resp.responseCode)
// Change port // Change port
`when`(sp.getInt("communication_http_port", 28891)).thenReturn(28893) `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 reqUri2 = URI("http://127.0.0.1:28893/get")
val resp2 = reqUri2.toURL().openConnection() as HttpURLConnection val resp2 = reqUri2.toURL().openConnection() as HttpURLConnection
assertEquals(200, resp2.responseCode) assertEquals(200, resp2.responseCode)
`when`(sp.getBoolean("communication_http", false)).thenReturn(false) `when`(sp.getBoolean("communication_http", false)).thenReturn(false)
gp.setupHttpServer() gp.setupHttpServer(Duration.ofSeconds(10))
assertThrows(ConnectException::class.java) { assertThrows(ConnectException::class.java) {
(reqUri2.toURL().openConnection() as HttpURLConnection).responseCode (reqUri2.toURL().openConnection() as HttpURLConnection).responseCode
} }
@ -177,7 +188,7 @@ class GarminPluginTest: TestBase() {
@Test @Test
fun setupHttpServer_disabled() { fun setupHttpServer_disabled() {
gp.setupHttpServer() gp.setupHttpServer(Duration.ofSeconds(10))
val reqUri = URI("http://127.0.0.1:28890/get") val reqUri = URI("http://127.0.0.1:28890/get")
assertThrows(ConnectException::class.java) { assertThrows(ConnectException::class.java) {
(reqUri.toURL().openConnection() as HttpURLConnection).responseCode (reqUri.toURL().openConnection() as HttpURLConnection).responseCode
@ -252,7 +263,7 @@ class GarminPluginTest: TestBase() {
gp.onConnectDevice(device) gp.onConnectDevice(device)
val captor = ArgumentCaptor.forClass(Any::class.java) 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") @Suppress("UNCHECKED_CAST")
val r = captor.value as Map<String, Any> val r = captor.value as Map<String, Any>
assertEquals("foo", r["key"]) assertEquals("foo", r["key"])
@ -357,4 +368,65 @@ 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(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<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","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
}
} }

View file

@ -5,6 +5,7 @@ import app.aaps.core.interfaces.aps.Loop
import app.aaps.core.interfaces.constraints.Constraint import app.aaps.core.interfaces.constraints.Constraint
import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.constraints.ConstraintsChecker
import app.aaps.core.interfaces.db.GlucoseUnit 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.IobCobCalculator
import app.aaps.core.interfaces.iob.IobTotal import app.aaps.core.interfaces.iob.IobTotal
import app.aaps.core.interfaces.logging.UserEntryLogger 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.pump.DetailedBolusInfo
import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.queue.CommandQueue
import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.core.main.graph.OverviewData
import app.aaps.database.ValueWrapper import app.aaps.database.ValueWrapper
import app.aaps.database.entities.EffectiveProfileSwitch import app.aaps.database.entities.EffectiveProfileSwitch
import app.aaps.database.entities.GlucoseValue import app.aaps.database.entities.GlucoseValue
@ -54,6 +56,7 @@ class LoopHubTest: TestBase() {
@Mock lateinit var repo: AppRepository @Mock lateinit var repo: AppRepository
@Mock lateinit var userEntryLogger: UserEntryLogger @Mock lateinit var userEntryLogger: UserEntryLogger
@Mock lateinit var sp: SP @Mock lateinit var sp: SP
@Mock lateinit var overviewData: OverviewData
private lateinit var loopHub: LoopHubImpl private lateinit var loopHub: LoopHubImpl
private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC"))
@ -62,7 +65,7 @@ class LoopHubTest: TestBase() {
fun setup() { fun setup() {
loopHub = LoopHubImpl( loopHub = LoopHubImpl(
aapsLogger, commandQueue, constraints, iobCobCalculator, loop, aapsLogger, commandQueue, constraints, iobCobCalculator, loop,
profileFunction, repo, userEntryLogger, sp profileFunction, repo, userEntryLogger, sp, overviewData
) )
loopHub.clock = clock loopHub.clock = clock
} }
@ -76,9 +79,10 @@ class LoopHubTest: TestBase() {
verifyNoMoreInteractions(profileFunction) verifyNoMoreInteractions(profileFunction)
verifyNoMoreInteractions(repo) verifyNoMoreInteractions(repo)
verifyNoMoreInteractions(userEntryLogger) verifyNoMoreInteractions(userEntryLogger)
verifyNoMoreInteractions(overviewData)
} }
@Test @Test
fun testCurrentProfile() { fun testCurrentProfile() {
val profile = mock(Profile::class.java) val profile = mock(Profile::class.java)
`when`(profileFunction.getProfile()).thenReturn(profile) `when`(profileFunction.getProfile()).thenReturn(profile)
@ -109,6 +113,22 @@ class LoopHubTest: TestBase() {
verify(iobCobCalculator, times(1)).calculateIobFromBolus() 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 @Test
fun testIsConnected() { fun testIsConnected() {
`when`(loop.isDisconnected).thenReturn(false) `when`(loop.isDisconnected).thenReturn(false)
@ -247,4 +267,4 @@ class LoopHubTest: TestBase() {
samplingStart, samplingEnd, 101, "Test Device") samplingStart, samplingEnd, 101, "Test Device")
verify(repo).runTransaction(InsertOrUpdateHeartRateTransaction(hr)) verify(repo).runTransaction(InsertOrUpdateHeartRateTransaction(hr))
} }
} }

View file

@ -153,7 +153,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.ClearAlarm, PumpHistoryEntryType.ClearAlarm,
PumpHistoryEntryType.ChangeAlarmNotifyMode, PumpHistoryEntryType.ChangeAlarmNotifyMode,
PumpHistoryEntryType.EnableDisableRemote, PumpHistoryEntryType.EnableDisableRemote,
PumpHistoryEntryType.BGReceived,
PumpHistoryEntryType.SensorAlert, PumpHistoryEntryType.SensorAlert,
PumpHistoryEntryType.ChangeTimeFormat, PumpHistoryEntryType.ChangeTimeFormat,
PumpHistoryEntryType.ChangeReservoirWarningTime, PumpHistoryEntryType.ChangeReservoirWarningTime,
@ -188,7 +187,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.ChangeWatchdogEnable, PumpHistoryEntryType.ChangeWatchdogEnable,
PumpHistoryEntryType.ChangeOtherDeviceID, PumpHistoryEntryType.ChangeOtherDeviceID,
PumpHistoryEntryType.ReadOtherDevicesIDs, PumpHistoryEntryType.ReadOtherDevicesIDs,
PumpHistoryEntryType.BGReceived512,
PumpHistoryEntryType.SensorStatus, PumpHistoryEntryType.SensorStatus,
PumpHistoryEntryType.ReadCaptureEventEnabled, PumpHistoryEntryType.ReadCaptureEventEnabled,
PumpHistoryEntryType.ChangeCaptureEventEnable, PumpHistoryEntryType.ChangeCaptureEventEnable,
@ -206,6 +204,12 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.UnabsorbedInsulin, PumpHistoryEntryType.UnabsorbedInsulin,
PumpHistoryEntryType.UnabsorbedInsulin512 -> RecordDecodeStatus.Ignored PumpHistoryEntryType.UnabsorbedInsulin512 -> RecordDecodeStatus.Ignored
PumpHistoryEntryType.BGReceived,
PumpHistoryEntryType.BGReceived512 -> {
decodeBgReceived(entry)
RecordDecodeStatus.OK
}
PumpHistoryEntryType.DailyTotals522, PumpHistoryEntryType.DailyTotals522,
PumpHistoryEntryType.DailyTotals523, PumpHistoryEntryType.DailyTotals523,
PumpHistoryEntryType.DailyTotals515, PumpHistoryEntryType.DailyTotals515,
@ -297,7 +301,9 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
} }
private fun decodeBatteryActivity(entry: PumpHistoryEntry) { 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 { private fun decodeBasalProfileStart(entry: PumpHistoryEntry): RecordDecodeStatus {
@ -407,8 +413,11 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
} }
private fun decodeBgReceived(entry: PumpHistoryEntry) { private fun decodeBgReceived(entry: PumpHistoryEntry) {
entry.addDecodedData("amount", (ByteUtil.asUINT8(entry.getRawDataByIndex(0)) shl 3) + (ByteUtil.asUINT8(entry.getRawDataByIndex(3)) shr 5)) val glucoseMgdl = (ByteUtil.asUINT8(entry.head[0]) shl 3) + (ByteUtil.asUINT8(entry.datetime[2]) shr 5)
entry.addDecodedData("meter", ByteUtil.substring(entry.rawData, 6, 3)) // index moved from 1 -> 0 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) { private fun decodeCalBGForPH(entry: PumpHistoryEntry) {

View file

@ -4,6 +4,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.notifications.Notification import app.aaps.core.interfaces.notifications.Notification
import app.aaps.core.interfaces.plugin.ActivePlugin 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.DetailedBolusInfo
import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.PumpSync
import app.aaps.core.interfaces.pump.defs.PumpType import app.aaps.core.interfaces.pump.defs.PumpType
@ -67,7 +68,8 @@ class MedtronicHistoryData @Inject constructor(
val medtronicPumpStatus: MedtronicPumpStatus, val medtronicPumpStatus: MedtronicPumpStatus,
private val pumpSync: PumpSync, private val pumpSync: PumpSync,
private val pumpSyncStorage: PumpSyncStorage, private val pumpSyncStorage: PumpSyncStorage,
private val uiInteraction: UiInteraction private val uiInteraction: UiInteraction,
private val profileUtil: ProfileUtil
) { ) {
val allHistory: MutableList<PumpHistoryEntry> = mutableListOf() val allHistory: MutableList<PumpHistoryEntry> = mutableListOf()
@ -322,6 +324,17 @@ class MedtronicHistoryData @Inject constructor(
* Process History Data: Boluses(Treatments), TDD, TBRs, Suspend-Resume (or other pump stops: battery, prime) * Process History Data: Boluses(Treatments), TDD, TBRs, Suspend-Resume (or other pump stops: battery, prime)
*/ */
fun processNewHistoryData() { fun processNewHistoryData() {
// Finger BG (for adding entry to careportal)
val bgRecords: MutableList<PumpHistoryEntry> = 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) // Prime (for resetting autosense)
val primeRecords: MutableList<PumpHistoryEntry> = getFilteredItems(PumpHistoryEntryType.Prime) val primeRecords: MutableList<PumpHistoryEntry> = getFilteredItems(PumpHistoryEntryType.Prime)
@ -347,6 +360,18 @@ class MedtronicHistoryData @Inject constructor(
} }
} }
// BatteryChange
val batteryChangeRecords: MutableList<PumpHistoryEntry> = 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 // TDD
val tdds: MutableList<PumpHistoryEntry> = getFilteredItems(setOf(PumpHistoryEntryType.EndResultTotals, getTDDType())) val tdds: MutableList<PumpHistoryEntry> = getFilteredItems(setOf(PumpHistoryEntryType.EndResultTotals, getTDDType()))
aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: TDD [count=%d, items=%s]", tdds.size, gson.toJson(tdds))) 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<PumpHistoryEntry>) {
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<PumpHistoryEntry>) { private fun processPrime(primeRecords: List<PumpHistoryEntry>) {
val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -30) val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -30)
var lastPrimeRecordTime = 0L var lastPrimeRecordTime = 0L
@ -456,6 +509,35 @@ class MedtronicHistoryData @Inject constructor(
} }
} }
private fun processBatteryChange(batteryChangeRecords: List<PumpHistoryEntry>) {
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) { private fun uploadCareportalEventIfFoundInHistory(historyRecord: PumpHistoryEntry, eventSP: String, eventType: DetailedBolusInfo.EventType) {
val lastPrimeFromAAPS = sp.getLong(eventSP, 0L) val lastPrimeFromAAPS = sp.getLong(eventSP, 0L)
if (historyRecord.atechDateTime != lastPrimeFromAAPS) { if (historyRecord.atechDateTime != lastPrimeFromAAPS) {

View file

@ -30,5 +30,6 @@ object MedtronicConst {
const val LastPumpHistoryEntry = StatsPrefix + "pump_history_entry" const val LastPumpHistoryEntry = StatsPrefix + "pump_history_entry"
const val LastPrime = StatsPrefix + "last_sent_prime" const val LastPrime = StatsPrefix + "last_sent_prime"
const val LastRewind = StatsPrefix + "last_sent_rewind" const val LastRewind = StatsPrefix + "last_sent_rewind"
const val LastBatteryChange = StatsPrefix + "last_sent_battery_change"
} }
} }

View file

@ -4,7 +4,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin
import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.PumpSync
import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.resources.ResourceHelper
import app.aaps.core.interfaces.sharedPreferences.SP 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.AndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil 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.Answers
import org.mockito.Mock import org.mockito.Mock
open class MedtronicTestBase : TestBase() { open class MedtronicTestBase : TestBaseWithProfile() {
var rileyLinkUtil = RileyLinkUtil() var rileyLinkUtil = RileyLinkUtil()
@Mock lateinit var pumpSync: PumpSync @Mock lateinit var pumpSync: PumpSync
@Mock lateinit var pumpSyncStorage: PumpSyncStorage @Mock lateinit var pumpSyncStorage: PumpSyncStorage
@Mock(answer = Answers.RETURNS_DEEP_STUBS) lateinit var activePlugin: ActivePlugin @Mock(answer = Answers.RETURNS_DEEP_STUBS) override lateinit var activePlugin: ActivePlugin
@Mock lateinit var sp: SP
@Mock lateinit var rh: ResourceHelper
lateinit var medtronicUtil: MedtronicUtil lateinit var medtronicUtil: MedtronicUtil
lateinit var decoder: MedtronicPumpHistoryDecoder lateinit var decoder: MedtronicPumpHistoryDecoder
@ -53,6 +51,24 @@ open class MedtronicTestBase : TestBase() {
} }
fun getPumpHistoryEntryFromData(vararg elements: Int): PumpHistoryEntry {
val data: MutableList<Byte> = 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<PumpHistoryEntry>): MutableList<PumpHistoryEntry> { private fun preProcessTBRs(tbrsInput: MutableList<PumpHistoryEntry>): MutableList<PumpHistoryEntry> {
val tbrs: MutableList<PumpHistoryEntry> = mutableListOf() val tbrs: MutableList<PumpHistoryEntry> = mutableListOf()
val map: MutableMap<String?, PumpHistoryEntry?> = HashMap() val map: MutableMap<String?, PumpHistoryEntry?> = HashMap()

View file

@ -40,7 +40,7 @@ import org.mockito.Mock
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
medtronicHistoryData = MedtronicHistoryData( medtronicHistoryData = MedtronicHistoryData(
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil
) )

View file

@ -1,8 +1,16 @@
package info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump 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.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 com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mock
import org.mockito.Mockito.`when`
/** /**
* Created by andy on 4/9/19. * Created by andy on 4/9/19.
@ -10,6 +18,16 @@ import org.junit.jupiter.api.Test
*/ */
class PumpHistoryEntryUTest : MedtronicTestBase() { 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 @Test
fun checkIsAfter() { fun checkIsAfter() {
val dateObject = 20191010000000L val dateObject = 20191010000000L
@ -18,4 +36,21 @@ class PumpHistoryEntryUTest : MedtronicTestBase() {
phe.atechDateTime = dateObject phe.atechDateTime = dateObject
assertThat(phe.isAfter(queryObject)).isTrue() 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)
}
} }

View file

@ -1,18 +1,24 @@
package info.nightscout.androidaps.plugins.pump.medtronic.data 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.interfaces.ui.UiInteraction
import app.aaps.core.utils.DateTimeUtil
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.internal.LinkedTreeMap import com.google.gson.internal.LinkedTreeMap
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase 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.MedtronicPumpHistoryDecoder
import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntry 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.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.driver.MedtronicPumpStatus
import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import java.lang.reflect.Type import java.lang.reflect.Type
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -24,6 +30,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction) medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction)
`when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel)
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
} }
@ -32,7 +39,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
val unitToTest = MedtronicHistoryData( val unitToTest = MedtronicHistoryData(
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil
) )
val gson = Gson() val gson = Gson()
@ -75,7 +82,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
medtronicUtil, decoder, medtronicUtil, decoder,
medtronicPumpStatus, medtronicPumpStatus,
pumpSync, pumpSync,
pumpSyncStorage, uiInteraction pumpSyncStorage, uiInteraction, profileUtil
) )
val gson = Gson() 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
)
}
} }

View file

@ -158,6 +158,7 @@
<string name="omnipod_common_pod_status_activation_time_exceeded">Activation time exceeded</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Activation time exceeded</string>
<string name="omnipod_common_pod_status_inactive">Inactive</string> <string name="omnipod_common_pod_status_inactive">Inactive</string>
<string name="omnipod_common_pod_status_pod_fault_description">Pod Fault: %1$03d %2$s</string> <string name="omnipod_common_pod_status_pod_fault_description">Pod Fault: %1$03d %2$s</string>
<string name="omnipod_common_pod_status_normal">Normal</string>
<!-- Omnipod - Commands --> <!-- Omnipod - Commands -->
<string name="omnipod_common_cmd_deactivate_pod">Deactivate Pod</string> <string name="omnipod_common_cmd_deactivate_pod">Deactivate Pod</string>

View file

@ -991,9 +991,9 @@ class OmnipodDashPumpPlugin @Inject constructor(
val extended = JSONObject() val extended = JSONObject()
try { try {
val podStatus = when { val podStatus = when {
podStateManager.isPodRunning && podStateManager.isSuspended -> "suspended" podStateManager.isPodRunning && podStateManager.isSuspended -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_suspended).lowercase()
podStateManager.isPodRunning -> "normal" podStateManager.isPodRunning -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_normal).lowercase()
else -> "no active Pod" 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("status", podStatus)
status.put("timestamp", dateUtil.toISOString(podStateManager.lastUpdatedSystem)) status.put("timestamp", dateUtil.toISOString(podStateManager.lastUpdatedSystem))

View file

@ -36,7 +36,7 @@ import org.mockito.invocation.InvocationOnMock
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
open class TestBaseWithProfile : TestBase() { open class TestBaseWithProfile : TestBase() {
@Mock lateinit var activePlugin: ActivePlugin @Mock open lateinit var activePlugin: ActivePlugin
@Mock lateinit var rh: ResourceHelper @Mock lateinit var rh: ResourceHelper
@Mock lateinit var iobCobCalculator: IobCobCalculator @Mock lateinit var iobCobCalculator: IobCobCalculator
@Mock lateinit var fabricPrivacy: FabricPrivacy @Mock lateinit var fabricPrivacy: FabricPrivacy

View file

@ -86,6 +86,9 @@ android {
versionName = Versions.appVersion + "-aapsclient2" versionName = Versions.appVersion + "-aapsclient2"
} }
} }
buildFeatures {
buildConfig = true
}
} }
allprojects { allprojects {