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.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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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="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>
|
||||||
|
|
|
@ -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