Add an automation heart rate trigger.
This commit is contained in:
parent
38cbaba621
commit
b9efaaf073
7 changed files with 219 additions and 1 deletions
6
core/main/src/main/res/drawable/ic_cp_heart_rate.xml
Normal file
6
core/main/src/main/res/drawable/ic_cp_heart_rate.xml
Normal 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>
|
|
@ -31,6 +31,7 @@ import info.nightscout.automation.triggers.TriggerBolusAgo
|
|||
import info.nightscout.automation.triggers.TriggerCOB
|
||||
import info.nightscout.automation.triggers.TriggerConnector
|
||||
import info.nightscout.automation.triggers.TriggerDelta
|
||||
import info.nightscout.automation.triggers.TriggerHeartRate
|
||||
import info.nightscout.automation.triggers.TriggerIob
|
||||
import info.nightscout.automation.triggers.TriggerLocation
|
||||
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.TriggerTimeRange
|
||||
import info.nightscout.automation.triggers.TriggerWifiSsid
|
||||
import info.nightscout.automation.ui.TimerUtil
|
||||
import info.nightscout.core.utils.fabric.FabricPrivacy
|
||||
import info.nightscout.interfaces.Config
|
||||
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.PluginType
|
||||
import info.nightscout.interfaces.queue.Callback
|
||||
import info.nightscout.automation.ui.TimerUtil
|
||||
import info.nightscout.rx.AapsSchedulers
|
||||
import info.nightscout.rx.bus.RxBus
|
||||
import info.nightscout.rx.events.EventBTChange
|
||||
|
@ -406,6 +407,7 @@ class AutomationPlugin @Inject constructor(
|
|||
TriggerBolusAgo(injector),
|
||||
TriggerPumpLastConnection(injector),
|
||||
TriggerBTDevice(injector),
|
||||
TriggerHeartRate(injector),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import info.nightscout.automation.triggers.TriggerCOB
|
|||
import info.nightscout.automation.triggers.TriggerConnector
|
||||
import info.nightscout.automation.triggers.TriggerDelta
|
||||
import info.nightscout.automation.triggers.TriggerDummy
|
||||
import info.nightscout.automation.triggers.TriggerHeartRate
|
||||
import info.nightscout.automation.triggers.TriggerIob
|
||||
import info.nightscout.automation.triggers.TriggerLocation
|
||||
import info.nightscout.automation.triggers.TriggerProfilePercent
|
||||
|
@ -75,6 +76,7 @@ abstract class AutomationModule {
|
|||
@ContributesAndroidInjector abstract fun triggerConnectorInjector(): TriggerConnector
|
||||
@ContributesAndroidInjector abstract fun triggerDeltaInjector(): TriggerDelta
|
||||
@ContributesAndroidInjector abstract fun triggerDummyInjector(): TriggerDummy
|
||||
@ContributesAndroidInjector abstract fun triggerHeartRateInjector(): TriggerHeartRate
|
||||
@ContributesAndroidInjector abstract fun triggerIobInjector(): TriggerIob
|
||||
@ContributesAndroidInjector abstract fun triggerLocationInjector(): TriggerLocation
|
||||
@ContributesAndroidInjector abstract fun triggerProfilePercentInjector(): TriggerProfilePercent
|
||||
|
|
|
@ -95,6 +95,7 @@ abstract class Trigger(val injector: HasAndroidInjector) {
|
|||
TriggerConnector::class.java.simpleName -> TriggerConnector(injector).fromJSON(data.toString())
|
||||
TriggerDelta::class.java.simpleName -> TriggerDelta(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())
|
||||
TriggerProfilePercent::class.java.simpleName -> TriggerProfilePercent(injector).fromJSON(data.toString())
|
||||
TriggerPumpLastConnection::class.java.simpleName -> TriggerPumpLastConnection(injector).fromJSON(data.toString())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -94,6 +94,8 @@
|
|||
<string name="lastboluscompared">Last bolus time %1$s %2$s min ago</string>
|
||||
<string name="triggercoblabel">COB</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="distance_short">Dist [m]:</string>
|
||||
<string name="recurringTime">Recurring time</string>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue