Add an automation heart rate trigger.

This commit is contained in:
robertbuessow 2023-06-25 19:47:18 +02:00
parent 38cbaba621
commit b9efaaf073
7 changed files with 219 additions and 1 deletions

View file

@ -0,0 +1,6 @@
<vector android:height="40dp"
android:viewportHeight="108"
android:viewportWidth="123"
android:width="46dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ed1b24" android:fillType="evenOdd" android:pathData="M60.83,17.18c8,-8.35 13.62,-15.57 26,-17C110,-2.46 131.27,21.26 119.57,44.61c-3.33,6.65 -10.11,14.56 -17.61,22.32 -8.23,8.52 -17.34,16.87 -23.72,23.2l-17.4,17.26L46.46,93.55C29.16,76.89 1,55.92 0,29.94 -0.63,11.74 13.73,0.08 30.25,0.29c14.76,0.2 21,7.54 30.58,16.89Z"/>
</vector>

View file

@ -31,6 +31,7 @@ import info.nightscout.automation.triggers.TriggerBolusAgo
import info.nightscout.automation.triggers.TriggerCOB import info.nightscout.automation.triggers.TriggerCOB
import info.nightscout.automation.triggers.TriggerConnector import info.nightscout.automation.triggers.TriggerConnector
import info.nightscout.automation.triggers.TriggerDelta import info.nightscout.automation.triggers.TriggerDelta
import info.nightscout.automation.triggers.TriggerHeartRate
import info.nightscout.automation.triggers.TriggerIob import info.nightscout.automation.triggers.TriggerIob
import info.nightscout.automation.triggers.TriggerLocation import info.nightscout.automation.triggers.TriggerLocation
import info.nightscout.automation.triggers.TriggerProfilePercent import info.nightscout.automation.triggers.TriggerProfilePercent
@ -41,6 +42,7 @@ import info.nightscout.automation.triggers.TriggerTempTargetValue
import info.nightscout.automation.triggers.TriggerTime import info.nightscout.automation.triggers.TriggerTime
import info.nightscout.automation.triggers.TriggerTimeRange import info.nightscout.automation.triggers.TriggerTimeRange
import info.nightscout.automation.triggers.TriggerWifiSsid import info.nightscout.automation.triggers.TriggerWifiSsid
import info.nightscout.automation.ui.TimerUtil
import info.nightscout.core.utils.fabric.FabricPrivacy import info.nightscout.core.utils.fabric.FabricPrivacy
import info.nightscout.interfaces.Config import info.nightscout.interfaces.Config
import info.nightscout.interfaces.GlucoseUnit import info.nightscout.interfaces.GlucoseUnit
@ -53,7 +55,6 @@ import info.nightscout.interfaces.plugin.PluginBase
import info.nightscout.interfaces.plugin.PluginDescription import info.nightscout.interfaces.plugin.PluginDescription
import info.nightscout.interfaces.plugin.PluginType import info.nightscout.interfaces.plugin.PluginType
import info.nightscout.interfaces.queue.Callback import info.nightscout.interfaces.queue.Callback
import info.nightscout.automation.ui.TimerUtil
import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.AapsSchedulers
import info.nightscout.rx.bus.RxBus import info.nightscout.rx.bus.RxBus
import info.nightscout.rx.events.EventBTChange import info.nightscout.rx.events.EventBTChange
@ -406,6 +407,7 @@ class AutomationPlugin @Inject constructor(
TriggerBolusAgo(injector), TriggerBolusAgo(injector),
TriggerPumpLastConnection(injector), TriggerPumpLastConnection(injector),
TriggerBTDevice(injector), TriggerBTDevice(injector),
TriggerHeartRate(injector),
) )
} }

View file

@ -38,6 +38,7 @@ import info.nightscout.automation.triggers.TriggerCOB
import info.nightscout.automation.triggers.TriggerConnector import info.nightscout.automation.triggers.TriggerConnector
import info.nightscout.automation.triggers.TriggerDelta import info.nightscout.automation.triggers.TriggerDelta
import info.nightscout.automation.triggers.TriggerDummy import info.nightscout.automation.triggers.TriggerDummy
import info.nightscout.automation.triggers.TriggerHeartRate
import info.nightscout.automation.triggers.TriggerIob import info.nightscout.automation.triggers.TriggerIob
import info.nightscout.automation.triggers.TriggerLocation import info.nightscout.automation.triggers.TriggerLocation
import info.nightscout.automation.triggers.TriggerProfilePercent import info.nightscout.automation.triggers.TriggerProfilePercent
@ -75,6 +76,7 @@ abstract class AutomationModule {
@ContributesAndroidInjector abstract fun triggerConnectorInjector(): TriggerConnector @ContributesAndroidInjector abstract fun triggerConnectorInjector(): TriggerConnector
@ContributesAndroidInjector abstract fun triggerDeltaInjector(): TriggerDelta @ContributesAndroidInjector abstract fun triggerDeltaInjector(): TriggerDelta
@ContributesAndroidInjector abstract fun triggerDummyInjector(): TriggerDummy @ContributesAndroidInjector abstract fun triggerDummyInjector(): TriggerDummy
@ContributesAndroidInjector abstract fun triggerHeartRateInjector(): TriggerHeartRate
@ContributesAndroidInjector abstract fun triggerIobInjector(): TriggerIob @ContributesAndroidInjector abstract fun triggerIobInjector(): TriggerIob
@ContributesAndroidInjector abstract fun triggerLocationInjector(): TriggerLocation @ContributesAndroidInjector abstract fun triggerLocationInjector(): TriggerLocation
@ContributesAndroidInjector abstract fun triggerProfilePercentInjector(): TriggerProfilePercent @ContributesAndroidInjector abstract fun triggerProfilePercentInjector(): TriggerProfilePercent

View file

@ -95,6 +95,7 @@ abstract class Trigger(val injector: HasAndroidInjector) {
TriggerConnector::class.java.simpleName -> TriggerConnector(injector).fromJSON(data.toString()) TriggerConnector::class.java.simpleName -> TriggerConnector(injector).fromJSON(data.toString())
TriggerDelta::class.java.simpleName -> TriggerDelta(injector).fromJSON(data.toString()) TriggerDelta::class.java.simpleName -> TriggerDelta(injector).fromJSON(data.toString())
TriggerDummy::class.java.simpleName -> TriggerDummy(injector).fromJSON(data.toString()) TriggerDummy::class.java.simpleName -> TriggerDummy(injector).fromJSON(data.toString())
TriggerHeartRate::class.java.simpleName -> TriggerHeartRate(injector).fromJSON(data.toString())
TriggerLocation::class.java.simpleName -> TriggerLocation(injector).fromJSON(data.toString()) TriggerLocation::class.java.simpleName -> TriggerLocation(injector).fromJSON(data.toString())
TriggerProfilePercent::class.java.simpleName -> TriggerProfilePercent(injector).fromJSON(data.toString()) TriggerProfilePercent::class.java.simpleName -> TriggerProfilePercent(injector).fromJSON(data.toString())
TriggerPumpLastConnection::class.java.simpleName -> TriggerPumpLastConnection(injector).fromJSON(data.toString()) TriggerPumpLastConnection::class.java.simpleName -> TriggerPumpLastConnection(injector).fromJSON(data.toString())

View file

@ -0,0 +1,79 @@
package info.nightscout.automation.triggers
import android.widget.LinearLayout
import androidx.annotation.VisibleForTesting
import com.google.common.base.Optional
import dagger.android.HasAndroidInjector
import info.nightscout.automation.R
import info.nightscout.automation.elements.Comparator
import info.nightscout.automation.elements.InputDouble
import info.nightscout.automation.elements.LabelWithElement
import info.nightscout.automation.elements.LayoutBuilder
import info.nightscout.automation.elements.StaticLabel
import info.nightscout.interfaces.utils.JsonHelper
import info.nightscout.rx.logging.LTag
import org.json.JSONObject
import java.text.DecimalFormat
class TriggerHeartRate(injector: HasAndroidInjector) : Trigger(injector) {
@VisibleForTesting val averageHeartRateDurationMillis = 330 * 1000L
private val minValue = 30
private val maxValue = 250
var heartRate: InputDouble = InputDouble(80.0, minValue.toDouble(), maxValue.toDouble(), 10.0, DecimalFormat("1"))
var comparator: Comparator = Comparator(rh).apply {
value = Comparator.Compare.IS_EQUAL_OR_GREATER
}
override fun shouldRun(): Boolean {
if (comparator.value == Comparator.Compare.IS_NOT_AVAILABLE) {
aapsLogger.info(LTag.AUTOMATION, "HR ready, no limit set ${friendlyDescription()}")
return true
}
val start = dateUtil.now() - averageHeartRateDurationMillis
val hrs = repository.getHeartRatesFromTime(start)
val duration = hrs.takeUnless { it.isEmpty() }?.sumOf { hr -> hr.duration } ?: 0L
if (duration == 0L) {
aapsLogger.info(LTag.AUTOMATION, "HR not ready, no heart rate measured for ${friendlyDescription()}")
return false
}
val hr = hrs.sumOf { hr -> hr.beatsPerMinute * hr.duration } / duration.toDouble()
return comparator.value.check(hr, heartRate.value).also {
aapsLogger.info(LTag.AUTOMATION, "HR ${if (it) "" else "not "}ready for $hr for ${friendlyDescription()}")
}
}
override fun dataJSON(): JSONObject =
JSONObject()
.put("heartRate", heartRate.value)
.put("comparator", comparator.value.toString())
override fun fromJSON(data: String): Trigger {
val d = JSONObject(data)
heartRate.setValue(JsonHelper.safeGetDouble(d, "heartRate"))
comparator.setValue(Comparator.Compare.valueOf(JsonHelper.safeGetString(d, "comparator")!!))
return this
}
override fun friendlyName(): Int = R.string.triggerHeartRate
override fun friendlyDescription(): String =
rh.gs(R.string.triggerHeartRateDesc, rh.gs(comparator.value.stringRes), heartRate.value)
override fun icon(): Optional<Int> = Optional.of(info.nightscout.core.main.R.drawable.ic_cp_heart_rate)
override fun duplicate(): Trigger {
return TriggerHeartRate(injector).also { o ->
o.heartRate.setValue(heartRate.value)
o.comparator.setValue(comparator.value)
}
}
override fun generateDialog(root: LinearLayout) {
LayoutBuilder()
.add(StaticLabel(rh, R.string.triggerHeartRate, this))
.add(comparator)
.add(LabelWithElement(rh, rh.gs(R.string.triggerHeartRate) + ": ", "", heartRate))
.build(root)
}
}

View file

@ -94,6 +94,8 @@
<string name="lastboluscompared">Last bolus time %1$s %2$s min ago</string> <string name="lastboluscompared">Last bolus time %1$s %2$s min ago</string>
<string name="triggercoblabel">COB</string> <string name="triggercoblabel">COB</string>
<string name="cobcompared">COB %1$s %2$.0f</string> <string name="cobcompared">COB %1$s %2$.0f</string>
<string name="triggerHeartRate">Heart Rate</string>
<string name="triggerHeartRateDesc">HR %1$s %2$.0f</string>
<string name="iob_u">IOB [U]:</string> <string name="iob_u">IOB [U]:</string>
<string name="distance_short">Dist [m]:</string> <string name="distance_short">Dist [m]:</string>
<string name="recurringTime">Recurring time</string> <string name="recurringTime">Recurring time</string>

View file

@ -0,0 +1,126 @@
package info.nightscout.automation.triggers
import info.nightscout.automation.R
import info.nightscout.automation.elements.Comparator
import info.nightscout.database.entities.HeartRate
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
class TriggerHeartRateTest : TriggerTestBase() {
private var now = 1000L
@BeforeEach
fun mock() {
`when`(dateUtil.now()).thenReturn(now)
}
@Test
fun friendlyName() {
assertEquals(R.string.triggerHeartRate, TriggerHeartRate(injector).friendlyName())
}
@Test
fun friendlyDescription() {
val t = TriggerHeartRate(injector)
`when`(rh.gs(Comparator.Compare.IS_EQUAL_OR_GREATER.stringRes)).thenReturn(">")
`when`(rh.gs(R.string.triggerHeartRateDesc, ">", 80.0)).thenReturn("test")
assertEquals("test", t.friendlyDescription())
}
@Test
fun duplicate() {
val t = TriggerHeartRate(injector).apply {
heartRate.value = 100.0
comparator.value = Comparator.Compare.IS_GREATER
}
val dup = t.duplicate() as TriggerHeartRate
assertNotSame(t, dup)
assertEquals(100.0, dup.heartRate.value, 0.01)
assertEquals(Comparator.Compare.IS_GREATER, dup.comparator.value)
}
@Test
fun shouldRunNotAvailable() {
val t = TriggerHeartRate(injector).apply { comparator.value = Comparator.Compare.IS_NOT_AVAILABLE }
assertTrue(t.shouldRun())
verifyNoMoreInteractions(repository)
}
@Test
fun shouldRunNoHeartRate() {
val t = TriggerHeartRate(injector).apply {
heartRate.value = 100.0
comparator.value = Comparator.Compare.IS_GREATER
}
`when`(repository.getHeartRatesFromTime(now - t.averageHeartRateDurationMillis)).thenReturn(emptyList())
assertFalse(t.shouldRun())
verify(repository).getHeartRatesFromTime(now - t.averageHeartRateDurationMillis)
verifyNoMoreInteractions(repository)
}
@Test
fun shouldRunBelowThreshold() {
val t = TriggerHeartRate(injector).apply {
heartRate.value = 100.0
comparator.value = Comparator.Compare.IS_GREATER
}
val hrs = listOf(
HeartRate(duration = 300_000, timestamp = now - 300_000, beatsPerMinute = 80.0, device = "test"),
HeartRate(duration = 300_000, timestamp = now, beatsPerMinute = 60.0, device = "test"),
)
`when`(repository.getHeartRatesFromTime(now - t.averageHeartRateDurationMillis)).thenReturn(hrs)
assertFalse(t.shouldRun())
verify(repository).getHeartRatesFromTime(now - t.averageHeartRateDurationMillis)
verifyNoMoreInteractions(repository)
}
@Test
fun shouldRunTrigger() {
val t = TriggerHeartRate(injector).apply {
heartRate.value = 100.0
comparator.value = Comparator.Compare.IS_GREATER
}
val hrs = listOf(
HeartRate(duration = 300_000, timestamp = now, beatsPerMinute = 120.0, device = "test"),
)
`when`(repository.getHeartRatesFromTime(now - t.averageHeartRateDurationMillis)).thenReturn(hrs)
assertTrue(t.shouldRun())
verify(repository).getHeartRatesFromTime(now - t.averageHeartRateDurationMillis)
verifyNoMoreInteractions(repository)
}
@Test
fun toJSON() {
val t = TriggerHeartRate(injector).apply {
heartRate.value = 100.0
comparator.value = Comparator.Compare.IS_GREATER
}
assertEquals(Comparator.Compare.IS_GREATER, t.comparator.value)
assertEquals(
"""{"data":{"comparator":"IS_GREATER","heartRate":100},"type":"TriggerHeartRate"}""".trimMargin(),
t.toJSON()
)
}
@Test
fun fromJSON() {
val t = TriggerDummy(injector).instantiate(
JSONObject(
"""{"data":{"comparator":"IS_GREATER","heartRate":100},"type":"TriggerHeartRate"}"""
)
) as TriggerHeartRate
assertEquals(Comparator.Compare.IS_GREATER, t.comparator.value)
assertEquals(100.0, t.heartRate.value, 0.01)
}
}