BgQualityCheckPlugin

This commit is contained in:
Milos Kozak 2021-11-05 13:16:23 +01:00
parent 6fb83d4949
commit 2b021ffafc
10 changed files with 356 additions and 32 deletions

View file

@ -15,6 +15,7 @@ import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin
import info.nightscout.androidaps.plugins.aps.openAPSAMA.OpenAPSAMAPlugin
import info.nightscout.androidaps.plugins.aps.openAPSSMB.OpenAPSSMBPlugin
import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin
import info.nightscout.androidaps.plugins.constraints.bgQualityCheck.BgQualityCheckPlugin
import info.nightscout.androidaps.plugins.constraints.dstHelper.DstHelperPlugin
import info.nightscout.androidaps.plugins.constraints.objectives.ObjectivesPlugin
import info.nightscout.androidaps.plugins.constraints.safety.SafetyPlugin
@ -294,6 +295,12 @@ abstract class PluginsModule {
@IntKey(380)
abstract fun bindDstHelperPlugin(plugin: DstHelperPlugin): PluginBase
@Binds
@AllConfigs
@IntoMap
@IntKey(381)
abstract fun bindBgQualityCheckPlugin(plugin: BgQualityCheckPlugin): PluginBase
@Binds
@AllConfigs
@IntoMap

View file

@ -0,0 +1,93 @@
package info.nightscout.androidaps.plugins.constraints.bgQualityCheck
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.R
import info.nightscout.androidaps.interfaces.*
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.bus.RxBus
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.events.EventBucketedDataCreated
import info.nightscout.androidaps.utils.FabricPrivacy
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.rx.AapsSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.abs
@Singleton
class BgQualityCheckPlugin @Inject constructor(
injector: HasAndroidInjector,
aapsLogger: AAPSLogger,
rh: ResourceHelper,
private val rxBus: RxBus,
private val iobCobCalculator: IobCobCalculator,
private val aapsSchedulers: AapsSchedulers,
private val fabricPrivacy: FabricPrivacy
) : PluginBase(
PluginDescription()
.mainType(PluginType.CONSTRAINTS)
.neverVisible(true)
.alwaysEnabled(true)
.showInList(false)
.pluginName(R.string.dst_plugin_name),
aapsLogger, rh, injector
), Constraints {
private var disposable: CompositeDisposable = CompositeDisposable()
enum class State {
UNKNOWN,
FIVE_MIN_DATA,
RECALCULATED,
DOUBLED
}
override fun onStart() {
super.onStart()
disposable += rxBus
.toObservable(EventBucketedDataCreated::class.java)
.observeOn(aapsSchedulers.io)
.subscribe({ processBgData() }, fabricPrivacy::logException)
}
override fun onStop() {
super.onStop()
disposable.clear()
}
var state: State = State.UNKNOWN
// Return false if BG values are doubled
@Suppress("ReplaceGetOrSet")
override fun isLoopInvocationAllowed(value: Constraint<Boolean>): Constraint<Boolean> {
if (state == State.DOUBLED)
value.set(aapsLogger, false, "Doubled values in BGSource", this)
return value
}
fun processBgData() {
val readings = iobCobCalculator.ads.getBgReadingsDataTableCopy()
for (i in readings.indices)
if (i < readings.size - 2)
if (abs(readings[i].timestamp - readings[i + 1].timestamp) <= 1000) {
state = State.DOUBLED
return
}
if (iobCobCalculator.ads.lastUsed5minCalculation == true)
state = State.FIVE_MIN_DATA
else if (iobCobCalculator.ads.lastUsed5minCalculation == false)
state = State.RECALCULATED
else
state = State.UNKNOWN
}
fun icon(): Int =
when (state) {
State.UNKNOWN -> 0
State.FIVE_MIN_DATA -> 0
State.RECALCULATED -> R.drawable.ic_baseline_warning_24_yellow
State.DOUBLED -> R.drawable.ic_baseline_warning_24_red
}
}

View file

@ -21,11 +21,11 @@ import javax.inject.Singleton
class DstHelperPlugin @Inject constructor(
injector: HasAndroidInjector,
aapsLogger: AAPSLogger,
private var rxBus: RxBus,
private val rxBus: RxBus,
rh: ResourceHelper,
private var sp: SP,
private var activePlugin: ActivePlugin,
private var loopPlugin: LoopPlugin
private val sp: SP,
private val activePlugin: ActivePlugin,
private val loopPlugin: LoopPlugin
) : PluginBase(PluginDescription()
.mainType(PluginType.CONSTRAINTS)
.neverVisible(true)
@ -41,6 +41,7 @@ class DstHelperPlugin @Inject constructor(
}
//Return false if time to DST change happened in the last 3 hours.
@Suppress("ReplaceGetOrSet")
override fun isLoopInvocationAllowed(value: Constraint<Boolean>): Constraint<Boolean> {
val pump = activePlugin.activePump
if (pump.canHandleDST()) {
@ -52,9 +53,9 @@ class DstHelperPlugin @Inject constructor(
val snoozedTo: Long = sp.getLong(R.string.key_snooze_dst_in24h, 0L)
if (snoozedTo == 0L || System.currentTimeMillis() > snoozedTo) {
val notification = NotificationWithAction(injector, Notification.DST_IN_24H, rh.gs(R.string.dst_in_24h_warning), Notification.LOW)
notification.action(R.string.snooze, Runnable {
notification.action(R.string.snooze) {
sp.putLong(R.string.key_snooze_dst_in24h, System.currentTimeMillis() + T.hours(24).msecs())
})
}
rxBus.send(EventNewNotification(notification))
}
}
@ -67,9 +68,9 @@ class DstHelperPlugin @Inject constructor(
val snoozedTo: Long = sp.getLong(R.string.key_snooze_loopdisabled, 0L)
if (snoozedTo == 0L || System.currentTimeMillis() > snoozedTo) {
val notification = NotificationWithAction(injector, Notification.DST_LOOP_DISABLED, rh.gs(R.string.dst_loop_disabled_warning), Notification.LOW)
notification.action(R.string.snooze, Runnable {
notification.action(R.string.snooze) {
sp.putLong(R.string.key_snooze_loopdisabled, System.currentTimeMillis() + T.hours(24).msecs())
})
}
rxBus.send(EventNewNotification(notification))
}
} else {

View file

@ -50,6 +50,7 @@ import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin
import info.nightscout.androidaps.plugins.aps.loop.events.EventNewOpenLoopNotification
import info.nightscout.androidaps.plugins.bus.RxBus
import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker
import info.nightscout.androidaps.plugins.constraints.bgQualityCheck.BgQualityCheckPlugin
import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin
import info.nightscout.androidaps.plugins.general.nsclient.data.NSDeviceStatus
import info.nightscout.androidaps.plugins.general.overview.activities.QuickWizardListActivity
@ -122,6 +123,7 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList
@Inject lateinit var overviewData: OverviewData
@Inject lateinit var overviewPlugin: OverviewPlugin
@Inject lateinit var automationPlugin: AutomationPlugin
@Inject lateinit var bgQualityCheckPlugin: BgQualityCheckPlugin
private val disposable = CompositeDisposable()
@ -661,6 +663,15 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList
else binding.infoLayout.bg.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
binding.infoLayout.timeAgo.text = dateUtil.minAgo(rh, overviewData.lastBg?.timestamp)
binding.infoLayout.timeAgoShort.text = "(" + dateUtil.minAgoShort(overviewData.lastBg?.timestamp) + ")"
val qualityIcon = bgQualityCheckPlugin.icon()
if (qualityIcon != 0) {
binding.infoLayout.bgQuality.visibility = View.VISIBLE
binding.infoLayout.bgQuality.setImageResource(qualityIcon)
} else {
binding.infoLayout.bgQuality.visibility = View.GONE
}
}
OverviewData.Property.PROFILE -> {

View file

@ -13,11 +13,8 @@ import info.nightscout.androidaps.interfaces.PluginDescription
import info.nightscout.androidaps.interfaces.PluginType
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpPlugin
import info.nightscout.androidaps.utils.DateUtil
import info.nightscout.androidaps.utils.T
import info.nightscout.androidaps.utils.XDripBroadcast
import info.nightscout.androidaps.utils.buildHelper.BuildHelper
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.disposables.CompositeDisposable
@ -33,20 +30,18 @@ class RandomBgPlugin @Inject constructor(
injector: HasAndroidInjector,
rh: ResourceHelper,
aapsLogger: AAPSLogger,
private val virtualPumpPlugin: VirtualPumpPlugin,
private val buildHelper: BuildHelper,
private val sp: SP,
private val dateUtil: DateUtil,
private val repository: AppRepository,
private val xDripBroadcast: XDripBroadcast
) : PluginBase(PluginDescription()
.mainType(PluginType.BGSOURCE)
.fragmentClass(BGSourceFragment::class.java.name)
.pluginIcon(R.drawable.ic_dice)
.pluginName(R.string.randombg)
.shortName(R.string.randombg_short)
.preferencesId(R.xml.pref_bgsource)
.description(R.string.description_source_randombg),
) : PluginBase(
PluginDescription()
.mainType(PluginType.BGSOURCE)
.fragmentClass(BGSourceFragment::class.java.name)
.pluginIcon(R.drawable.ic_dice)
.pluginName(R.string.randombg)
.shortName(R.string.randombg_short)
.preferencesId(R.xml.pref_bgsource)
.description(R.string.description_source_randombg),
aapsLogger, rh, injector
), BgSource {
@ -58,7 +53,7 @@ class RandomBgPlugin @Inject constructor(
const val interval = 5L // minutes
const val min = 70 // mgdl
const val max = 190 // mgdl
const val period = 90.0 // minutes
const val period = 120.0 // minutes
}
init {
@ -94,15 +89,18 @@ class RandomBgPlugin @Inject constructor(
}
private fun handleNewData() {
if (!isEnabled(PluginType.BGSOURCE)) return
if (!isEnabled()) return
val cal = GregorianCalendar()
val currentMinute = cal.get(Calendar.MINUTE) + (cal.get(Calendar.HOUR_OF_DAY) % 2) * 60
val currentMinute = cal[Calendar.MINUTE] + (cal[Calendar.HOUR_OF_DAY] % 2) * 60
val bgMgdl = min + ((max - min) + (max - min) * sin(currentMinute / period * 2 * PI)) / 2
cal[Calendar.MILLISECOND] = 0
cal[Calendar.SECOND] = 0
cal[Calendar.MINUTE] -= cal[Calendar.MINUTE] % 5
val glucoseValues = mutableListOf<CgmSourceTransaction.TransactionGlucoseValue>()
glucoseValues += CgmSourceTransaction.TransactionGlucoseValue(
timestamp = dateUtil.now(),
timestamp = cal.timeInMillis,
value = bgMgdl,
raw = 0.0,
noise = null,
@ -111,11 +109,11 @@ class RandomBgPlugin @Inject constructor(
)
disposable += repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), null))
.subscribe({ savedValues ->
savedValues.inserted.forEach {
xDripBroadcast(it)
aapsLogger.debug(LTag.DATABASE, "Inserted bg $it")
}
}, { aapsLogger.error(LTag.DATABASE, "Error while saving values from Random plugin", it) }
savedValues.inserted.forEach {
xDripBroadcast(it)
aapsLogger.debug(LTag.DATABASE, "Inserted bg $it")
}
}, { aapsLogger.error(LTag.DATABASE, "Error while saving values from Random plugin", it) }
)
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FF0000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFF00"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View file

@ -19,6 +19,14 @@
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/bg_quality"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="Blood glucose quality icon"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/delta_large"

View file

@ -0,0 +1,196 @@
package info.nightscout.androidaps.plugins.constraints.bgQualityCheck
import dagger.android.AndroidInjector
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.R
import info.nightscout.androidaps.TestBase
import info.nightscout.androidaps.database.entities.GlucoseValue
import info.nightscout.androidaps.interfaces.Constraint
import info.nightscout.androidaps.interfaces.IobCobCalculator
import info.nightscout.androidaps.plugins.bus.RxBus
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensDataStore
import info.nightscout.androidaps.utils.FabricPrivacy
import info.nightscout.androidaps.utils.T
import info.nightscout.androidaps.utils.resources.ResourceHelper
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import java.util.*
class BgQualityCheckPluginTest : TestBase() {
@Mock lateinit var rh: ResourceHelper
@Mock lateinit var iobCobCalculator: IobCobCalculator
@Mock lateinit var fabricPrivacy: FabricPrivacy
private lateinit var plugin: BgQualityCheckPlugin
val injector = HasAndroidInjector { AndroidInjector { } }
private val autosensDataStore = AutosensDataStore()
@Before
fun mock() {
plugin = BgQualityCheckPlugin(injector, aapsLogger, rh, RxBus(aapsSchedulers, aapsLogger), iobCobCalculator, aapsSchedulers, fabricPrivacy)
Mockito.`when`(iobCobCalculator.ads).thenReturn(autosensDataStore)
}
@Test
fun runTest() {
autosensDataStore.lastUsed5minCalculation = null
plugin.processBgData()
Assert.assertEquals(BgQualityCheckPlugin.State.UNKNOWN, plugin.state)
Assert.assertEquals(0, plugin.icon())
autosensDataStore.lastUsed5minCalculation = true
plugin.processBgData()
Assert.assertEquals(BgQualityCheckPlugin.State.FIVE_MIN_DATA, plugin.state)
Assert.assertEquals(0, plugin.icon())
autosensDataStore.lastUsed5minCalculation = false
plugin.processBgData()
Assert.assertEquals(BgQualityCheckPlugin.State.RECALCULATED, plugin.state)
Assert.assertEquals(R.drawable.ic_baseline_warning_24_yellow, plugin.icon())
val superData: MutableList<GlucoseValue> = ArrayList()
superData.add(GlucoseValue(raw = 0.0, noise = 0.0, value = 100.0, timestamp = T.mins(20).msecs(), sourceSensor = GlucoseValue.SourceSensor.UNKNOWN, trendArrow = GlucoseValue.TrendArrow.FLAT))
superData.add(GlucoseValue(raw = 0.0, noise = 0.0, value = 100.0, timestamp = T.mins(15).msecs(), sourceSensor = GlucoseValue.SourceSensor.UNKNOWN, trendArrow = GlucoseValue.TrendArrow.FLAT))
superData.add(GlucoseValue(raw = 0.0, noise = 0.0, value = 100.0, timestamp = T.mins(10).msecs(), sourceSensor = GlucoseValue.SourceSensor.UNKNOWN, trendArrow = GlucoseValue.TrendArrow.FLAT))
superData.add(GlucoseValue(raw = 0.0, noise = 0.0, value = 100.0, timestamp = T.mins(5).msecs(), sourceSensor = GlucoseValue.SourceSensor.UNKNOWN, trendArrow = GlucoseValue.TrendArrow.FLAT))
autosensDataStore.bgReadings = superData
autosensDataStore.lastUsed5minCalculation = true
plugin.processBgData()
Assert.assertEquals(BgQualityCheckPlugin.State.FIVE_MIN_DATA, plugin.state)
autosensDataStore.lastUsed5minCalculation = false
plugin.processBgData()
Assert.assertEquals(BgQualityCheckPlugin.State.RECALCULATED, plugin.state)
val duplicatedData: MutableList<GlucoseValue> = ArrayList()
duplicatedData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(20).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
duplicatedData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(20).msecs() + 1,
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
duplicatedData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(10).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
duplicatedData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(15).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
duplicatedData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(5).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
autosensDataStore.bgReadings = duplicatedData
autosensDataStore.lastUsed5minCalculation = true
plugin.processBgData()
Assert.assertEquals(BgQualityCheckPlugin.State.DOUBLED, plugin.state)
Assert.assertEquals(R.drawable.ic_baseline_warning_24_red, plugin.icon())
val identicalData: MutableList<GlucoseValue> = ArrayList()
identicalData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(20).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
identicalData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(20).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
identicalData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(10).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
identicalData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(15).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
identicalData.add(
GlucoseValue(
raw = 0.0,
noise = 0.0,
value = 100.0,
timestamp = T.mins(5).msecs(),
sourceSensor = GlucoseValue.SourceSensor.UNKNOWN,
trendArrow = GlucoseValue.TrendArrow.FLAT
)
)
autosensDataStore.bgReadings = identicalData
autosensDataStore.lastUsed5minCalculation = false
plugin.processBgData()
Assert.assertEquals(BgQualityCheckPlugin.State.DOUBLED, plugin.state)
}
@Test
fun isLoopInvocationAllowedTest() {
plugin.state = BgQualityCheckPlugin.State.UNKNOWN
Assert.assertEquals(true, plugin.isLoopInvocationAllowed(Constraint(true)).value())
plugin.state = BgQualityCheckPlugin.State.FIVE_MIN_DATA
Assert.assertEquals(true, plugin.isLoopInvocationAllowed(Constraint(true)).value())
plugin.state = BgQualityCheckPlugin.State.RECALCULATED
Assert.assertEquals(true, plugin.isLoopInvocationAllowed(Constraint(true)).value())
plugin.state = BgQualityCheckPlugin.State.DOUBLED
Assert.assertEquals(false, plugin.isLoopInvocationAllowed(Constraint(true)).value())
}
}

View file

@ -19,7 +19,7 @@ import kotlin.math.roundToLong
class AutosensDataStore {
private val dataLock = Any()
private var lastUsed5minCalculation: Boolean? = null // true if used 5min bucketed data
var lastUsed5minCalculation: Boolean? = null // true if used 5min bucketed data
// we need to make sure that bucketed_data will always have the same timestamp for correct use of cached values
// once referenceTime != null all bucketed data should be (x * 5min) from referenceTime