diff --git a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt index 30570b41c1de..783d58e21729 100644 --- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt +++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt @@ -24,6 +24,7 @@ import app.aaps.plugins.main.general.actions.ActionsPlugin import app.aaps.plugins.main.general.food.FoodPlugin import app.aaps.plugins.main.general.overview.OverviewPlugin import app.aaps.plugins.main.general.persistentNotification.PersistentNotificationPlugin +import app.aaps.plugins.main.general.remora.RemoraPlugin import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin import app.aaps.plugins.main.general.themes.ThemeSwitcherPlugin import app.aaps.plugins.main.iob.iobCobCalculator.IobCobCalculatorPlugin @@ -288,6 +289,12 @@ abstract class PluginsListModule { @IntKey(280) abstract fun bindSmsCommunicatorPlugin(plugin: SmsCommunicatorPlugin): PluginBase + @Binds + @NotNSClient + @IntoMap + @IntKey(285) + abstract fun bindRemoraPlugin(plugin: RemoraPlugin): PluginBase + @Binds @APS @IntoMap diff --git a/core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt b/core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt index 4c9677b8ba2d..567d32e3081f 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt @@ -75,6 +75,7 @@ enum class Sources { Garmin, Database, // for PersistenceLayer Unknown, //if necessary - SyaiTag + SyaiTag, + Remora ; } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt index f68f8519aff8..0fa1be50a04a 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt @@ -255,6 +255,16 @@ interface PersistenceLayer { */ fun getCarbsFromTimeToTimeExpanded(startTime: Long, endTime: Long, ascending: Boolean): List + /** + * Get carbs in time interval (eCarbs as single records) + * + * @param startTime from + * @param endTime to + * @param ascending sort order + * @return List of carbs + */ + fun getCarbsFromTimeToTimeNotExpanded(startTime: Long, endTime: Long, ascending: Boolean): List + /** * Get next changed record after id * @@ -1054,6 +1064,7 @@ interface PersistenceLayer { fun getTemporaryTargetByNSId(nsId: String): TT? fun getTemporaryTargetDataFromTime(timestamp: Long, ascending: Boolean): Single> + fun getTemporaryTargetDataFromTimeToTime(start: Long, end: Long, ascending: Boolean): Single> fun getTemporaryTargetDataIncludingInvalidFromTime(timestamp: Long, ascending: Boolean): Single> /** diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt index 4bd38b722a3d..e219ee18377c 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt @@ -6,4 +6,5 @@ interface SmsCommunicator { fun sendNotificationToAllNumbers(text: String): Boolean fun sendSMS(sms: Sms): Boolean fun isEnabled(): Boolean + fun invalidateMessage() } \ No newline at end of file diff --git a/core/objects/src/main/res/drawable/ic_remora.xml b/core/objects/src/main/res/drawable/ic_remora.xml new file mode 100644 index 000000000000..eaf34be29355 --- /dev/null +++ b/core/objects/src/main/res/drawable/ic_remora.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/database/impl/src/main/kotlin/app/aaps/database/AppRepository.kt b/database/impl/src/main/kotlin/app/aaps/database/AppRepository.kt index 18f1cb0afd6a..857505f61a7a 100644 --- a/database/impl/src/main/kotlin/app/aaps/database/AppRepository.kt +++ b/database/impl/src/main/kotlin/app/aaps/database/AppRepository.kt @@ -206,6 +206,11 @@ class AppRepository @Inject internal constructor( .map { if (!ascending) it.reversed() else it } .subscribeOn(Schedulers.io()) + fun getTemporaryTargetDataFromTimeToTime(start: Long, end: Long, ascending: Boolean): Single> = + database.temporaryTargetDao.getTemporaryTargetDataFromTimeToTime(start, end) + .map { if (!ascending) it.reversed() else it } + .subscribeOn(Schedulers.io()) + fun getTemporaryTargetDataIncludingInvalidFromTime(timestamp: Long, ascending: Boolean): Single> = database.temporaryTargetDao.getTemporaryTargetDataIncludingInvalidFromTime(timestamp) .map { if (!ascending) it.reversed() else it } @@ -591,6 +596,12 @@ class AppRepository @Inject internal constructor( .sort() .map { if (!ascending) it.reversed() else it } .subscribeOn(Schedulers.io()) + .subscribeOn(Schedulers.io()) + + fun getCarbsDataFromTimeToTimeNotExpanded(from: Long, to: Long, ascending: Boolean): Single> = + database.carbsDao.getCarbsFromTimeToTimeExpandable(from, to) + .map { if (!ascending) it.reversed() else it } + .subscribeOn(Schedulers.io()) fun getCarbsIncludingInvalidFromTime(timestamp: Long, ascending: Boolean): Single> = database.carbsDao.getCarbsIncludingInvalidFromTime(timestamp) diff --git a/database/impl/src/main/kotlin/app/aaps/database/daos/TemporaryTargetDao.kt b/database/impl/src/main/kotlin/app/aaps/database/daos/TemporaryTargetDao.kt index 2a4b318dc03f..7b380680d9c3 100644 --- a/database/impl/src/main/kotlin/app/aaps/database/daos/TemporaryTargetDao.kt +++ b/database/impl/src/main/kotlin/app/aaps/database/daos/TemporaryTargetDao.kt @@ -37,6 +37,9 @@ internal interface TemporaryTargetDao : TraceableDao { @Query("SELECT * FROM $TABLE_TEMPORARY_TARGETS WHERE unlikely(timestamp >= :timestamp) AND likely(isValid = 1) AND likely(referenceId IS NULL) ORDER BY timestamp ASC") fun getTemporaryTargetDataFromTime(timestamp: Long): Single> + @Query("SELECT * FROM $TABLE_TEMPORARY_TARGETS WHERE unlikely(timestamp + duration >= :start) AND unlikely(timestamp <= :end) AND likely(isValid = 1) AND likely(referenceId IS NULL) ORDER BY timestamp ASC") + fun getTemporaryTargetDataFromTimeToTime(start: Long, end: Long): Single> + @Query("SELECT * FROM $TABLE_TEMPORARY_TARGETS WHERE unlikely(timestamp >= :timestamp) AND likely(referenceId IS NULL) ORDER BY timestamp ASC") fun getTemporaryTargetDataIncludingInvalidFromTime(timestamp: Long): Single> diff --git a/database/impl/src/main/kotlin/app/aaps/database/entities/UserEntry.kt b/database/impl/src/main/kotlin/app/aaps/database/entities/UserEntry.kt index d3c63159ef4b..1fa6453e091a 100644 --- a/database/impl/src/main/kotlin/app/aaps/database/entities/UserEntry.kt +++ b/database/impl/src/main/kotlin/app/aaps/database/entities/UserEntry.kt @@ -194,6 +194,7 @@ data class UserEntry( Garmin, Database, //for PersistenceLayer Unknown, //if necessary + Remora ; companion object { diff --git a/database/persistence/src/main/kotlin/app/aaps/database/persistence/PersistenceLayerImpl.kt b/database/persistence/src/main/kotlin/app/aaps/database/persistence/PersistenceLayerImpl.kt index 3e4fa5f01ca1..bcb61b4ab7d6 100644 --- a/database/persistence/src/main/kotlin/app/aaps/database/persistence/PersistenceLayerImpl.kt +++ b/database/persistence/src/main/kotlin/app/aaps/database/persistence/PersistenceLayerImpl.kt @@ -348,6 +348,11 @@ class PersistenceLayerImpl @Inject constructor( .map { list -> list.asSequence().map { it.fromDb() }.toList() } .blockingGet() + override fun getCarbsFromTimeToTimeNotExpanded(startTime: Long, endTime: Long, ascending: Boolean): List = + repository.getCarbsDataFromTimeToTimeNotExpanded(startTime, endTime, ascending) + .map { list -> list.asSequence().map { it.fromDb() }.toList() } + .blockingGet() + override fun getNextSyncElementCarbs(id: Long): Maybe> = repository.getNextSyncElementCarbs(id) .map { pair -> Pair(pair.first.fromDb(), pair.second.fromDb()) } @@ -1400,6 +1405,9 @@ class PersistenceLayerImpl @Inject constructor( override fun getTemporaryTargetDataFromTime(timestamp: Long, ascending: Boolean): Single> = repository.getTemporaryTargetDataFromTime(timestamp, ascending).map { list -> list.asSequence().map { it.fromDb() }.toList() } + override fun getTemporaryTargetDataFromTimeToTime(start: Long, end: Long, ascending: Boolean): Single> = + repository.getTemporaryTargetDataFromTimeToTime(start, end, ascending).map { list -> list.asSequence().map { it.fromDb() }.toList() } + override fun getTemporaryTargetDataIncludingInvalidFromTime(timestamp: Long, ascending: Boolean): Single> = repository.getTemporaryTargetDataIncludingInvalidFromTime(timestamp, ascending).map { list -> list.asSequence().map { it.fromDb() }.toList() } diff --git a/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/SourcesExtension.kt b/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/SourcesExtension.kt index e6f773725ee4..b7139a213e8e 100644 --- a/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/SourcesExtension.kt +++ b/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/SourcesExtension.kt @@ -80,6 +80,7 @@ fun UserEntry.Sources.fromDb(): Sources = UserEntry.Sources.Unknown -> Sources.Unknown UserEntry.Sources.Ottai -> Sources.Ottai UserEntry.Sources.SyaiTag -> Sources.SyaiTag + UserEntry.Sources.Remora -> Sources.Remora } fun Sources.toDb(): UserEntry.Sources = @@ -159,5 +160,6 @@ fun Sources.toDb(): UserEntry.Sources = Sources.Unknown -> UserEntry.Sources.Unknown Sources.Ottai -> UserEntry.Sources.Ottai Sources.SyaiTag -> UserEntry.Sources.SyaiTag + Sources.Remora -> UserEntry.Sources.Remora } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index cf30656d2bcb..eceff4977d85 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -104,7 +104,7 @@ class CommandQueueImplementation @Inject constructor( private val decimalFormatter: DecimalFormatter, private val instantiator: Instantiator, private val jobName: CommandQueueName, - private val workManager: WorkManager + private val workManager: WorkManager, ) : CommandQueue { private val disposable = CompositeDisposable() @@ -167,7 +167,8 @@ class CommandQueueImplementation @Inject constructor( synchronized(queue) { for (i in queue.indices.reversed()) { if (queue[i].commandType == type) { - queue.removeAt(i) + queue.removeAt(i).callback + ?.result(instantiator.providePumpEnactResult().success(false).enacted(false))?.run() } } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/userEntry/UserEntryPresentationHelperImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/userEntry/UserEntryPresentationHelperImpl.kt index 3db0151083af..297e26e142d3 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/userEntry/UserEntryPresentationHelperImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/userEntry/UserEntryPresentationHelperImpl.kt @@ -116,6 +116,7 @@ class UserEntryPresentationHelperImpl @Inject constructor( Sources.BgFragment -> R.drawable.ic_aaps Sources.Ottai -> R.drawable.ic_ottai Sources.SyaiTag -> R.drawable.ic_syai_tag + Sources.Remora -> R.drawable.ic_remora } override fun actionToColoredString(action: Action): Spanned = when (action) { diff --git a/plugins/main/build.gradle.kts b/plugins/main/build.gradle.kts index 2e21a3531582..85f252a487f0 100644 --- a/plugins/main/build.gradle.kts +++ b/plugins/main/build.gradle.kts @@ -47,4 +47,6 @@ dependencies { ksp(libs.com.google.dagger.compiler) ksp(libs.com.google.dagger.android.processor) + + implementation("de.tebbeubben.remora:lib") } \ No newline at end of file diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt index 07f0e904e96c..b2c5012e5c3b 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt @@ -21,6 +21,7 @@ import dagger.android.ContributesAndroidInjector SkinsModule::class, ActionsModule::class, OverviewModule::class, + RemoraModule::class ] ) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/RemoraModule.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/RemoraModule.kt new file mode 100644 index 000000000000..3a60442bf75d --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/RemoraModule.kt @@ -0,0 +1,12 @@ +package app.aaps.plugins.main.di + +import app.aaps.plugins.main.general.remora.RemoraFragment +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +@Suppress("unused") +abstract class RemoraModule { + + @ContributesAndroidInjector abstract fun contributesRemoraFragment(): RemoraFragment +} \ No newline at end of file diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/RemoraFragment.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/RemoraFragment.kt new file mode 100644 index 000000000000..cc3f8bd309fe --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/RemoraFragment.kt @@ -0,0 +1,26 @@ +package app.aaps.plugins.main.general.remora + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import app.aaps.plugins.main.R +import dagger.android.support.DaggerFragment +import de.tebbeubben.remora.lib.ui.RemoraLibActivity + +class RemoraFragment : DaggerFragment() { + + @SuppressLint("MissingInflatedId") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.remora_fragment, container, false) + val launchActivityButton: Button = view.findViewById(R.id.launch_activity_button) + launchActivityButton.setOnClickListener { + val intent = Intent(activity, RemoraLibActivity::class.java) + startActivity(intent) + } + return view + } +} \ No newline at end of file diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/RemoraPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/RemoraPlugin.kt new file mode 100644 index 000000000000..1b65fa5f3420 --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/RemoraPlugin.kt @@ -0,0 +1,429 @@ +package app.aaps.plugins.main.general.remora + +import android.content.Context +import androidx.work.WorkManager +import app.aaps.core.data.configuration.Constants +import app.aaps.core.data.model.BS +import app.aaps.core.data.model.CA +import app.aaps.core.data.model.GlucoseUnit +import app.aaps.core.data.model.TT +import app.aaps.core.data.plugin.PluginType +import app.aaps.core.data.ue.Action +import app.aaps.core.data.ue.Sources +import app.aaps.core.data.ue.ValueWithUnit +import app.aaps.core.interfaces.aps.Loop +import app.aaps.core.interfaces.constraints.ConstraintsChecker +import app.aaps.core.interfaces.db.PersistenceLayer +import app.aaps.core.interfaces.iob.IobCobCalculator +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.overview.LastBgData +import app.aaps.core.interfaces.plugin.ActivePlugin +import app.aaps.core.interfaces.plugin.PluginBase +import app.aaps.core.interfaces.plugin.PluginDescription +import app.aaps.core.interfaces.profile.ProfileUtil +import app.aaps.core.interfaces.pump.DetailedBolusInfo +import app.aaps.core.interfaces.queue.Callback +import app.aaps.core.interfaces.queue.CommandQueue +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.AapsSchedulers +import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.events.Event +import app.aaps.core.interfaces.rx.events.EventDeviceStatusChange +import app.aaps.core.interfaces.rx.events.EventNewHistoryData +import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress +import app.aaps.core.interfaces.rx.events.EventProfileSwitchChanged +import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged +import app.aaps.core.interfaces.rx.events.EventRunningModeChange +import app.aaps.core.interfaces.rx.events.EventTempTargetChange +import app.aaps.core.interfaces.rx.events.EventTherapyEventChange +import app.aaps.core.interfaces.rx.events.EventUpdateOverviewGraph +import app.aaps.core.interfaces.smsCommunicator.SmsCommunicator +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.interfaces.utils.HardLimits +import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.objects.constraints.ConstraintObject +import app.aaps.plugins.main.R +import dagger.Lazy +import de.tebbeubben.remora.lib.LibraryMode +import de.tebbeubben.remora.lib.RemoraLib +import de.tebbeubben.remora.lib.commands.CommandHandler +import de.tebbeubben.remora.lib.commands.wrapError +import de.tebbeubben.remora.lib.commands.wrapSuccess +import de.tebbeubben.remora.lib.model.commands.RemoraCommand +import de.tebbeubben.remora.lib.model.commands.RemoraCommandData +import de.tebbeubben.remora.lib.model.commands.RemoraCommandError +import de.tebbeubben.remora.lib.model.commands.RemoraStatusSnapshot +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.rx3.asFlow +import kotlinx.coroutines.rx3.await +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlin.math.abs +import kotlin.math.roundToInt +import kotlin.time.Clock +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@OptIn(ExperimentalTime::class) +@Singleton +class RemoraPlugin @Inject constructor( + aapsLogger: AAPSLogger, + rh: ResourceHelper, + private val statusDataBuilder: StatusDataBuilder, + val activePlugin: ActivePlugin, + val aapsSchedulers: AapsSchedulers, + private val commandQueue: CommandQueue, + private val loop: Loop, + private val constraintsChecker: ConstraintsChecker, + private val preferences: Preferences, + private val persistenceLayer: PersistenceLayer, + private val profileUtil: ProfileUtil, + private val dateUtil: DateUtil, + context: Context, + private val rxBus: RxBus, + private val workManager: WorkManager, + private val iobCobCalculator: IobCobCalculator, + private val lastBgData: LastBgData, + private val hardLimits: HardLimits, + private val smsCommunicator: Lazy, +) : PluginBase( + PluginDescription() + .mainType(PluginType.GENERAL) + .fragmentClass(RemoraFragment::class.java.name) + .pluginIcon(app.aaps.core.objects.R.drawable.ic_remora) + .pluginName(R.string.remora) + .shortName(R.string.remora_shortname) + .description(R.string.description_remora), + aapsLogger, rh +), CommandHandler { + + private val remoraLib = RemoraLib.initialize(context, LibraryMode.MAIN) + + init { + remoraLib.setCommandHandler(this) + } + + private val bolusProgressFlow get() = rxBus.toObservable(EventOverviewBolusProgress::class.java).asFlow() + private val pumpStatusProgressFlow get() = rxBus.toObservable(EventPumpStatusChanged::class.java).asFlow() + private val commandQueueInfoFlow get() = workManager.getWorkInfosByTagFlow("CommandQueue") + private val calculationWorkflowInfoFlow get() = workManager.getWorkInfosByTagFlow("calculation") + + // These events will trigger re-sending the current status information to all followers + private val mergedEvents: Flow + get() = merge( + rxBus.toObservable(EventNewHistoryData::class.java).asFlow(), + rxBus.toObservable(EventTempTargetChange::class.java).asFlow(), + rxBus.toObservable(EventTherapyEventChange::class.java).asFlow(), + rxBus.toObservable(EventProfileSwitchChanged::class.java).asFlow(), + rxBus.toObservable(EventRunningModeChange::class.java).asFlow(), + rxBus.toObservable(EventDeviceStatusChange::class.java).asFlow(), + rxBus.toObservable(EventUpdateOverviewGraph::class.java).asFlow() + ) + + // This flow will be triggered by any of the events from `mergedEvents`, + // but waits for the command queue and calculation workflow to finish before triggering the update. + private val updateFlow: Flow = combine( + mergedEvents, + commandQueueInfoFlow, + calculationWorkflowInfoFlow + ) { _, commandQueueInfo, calcWorkflowInfo -> + commandQueueInfo.any { !it.state.isFinished } || calcWorkflowInfo.any { !it.state.isFinished } + } + .filter { !it } + .map { Unit } + + private var scope = CoroutineScope(Dispatchers.Default) + + @OptIn(FlowPreview::class) + override fun onStart() { + commandQueue.performing() + scope = CoroutineScope(Dispatchers.Default) + scope.launch { + remoraLib.startup() + updateFlow + .debounce(1000) + .collectLatest { + val statusData = statusDataBuilder.constructStatusData() + if (statusData != null) { + remoraLib.shareStatus(statusData) + } + } + } + } + + override fun onStop() { + scope.cancel() + runBlocking { + remoraLib.shutdown() + } + } + + @OptIn(ExperimentalTime::class) + override suspend fun validateStatusSnapshot(snapshot: RemoraStatusSnapshot): RemoraCommandError? { + val currentCob = iobCobCalculator.getCobInfo("Remora").displayCob + if (currentCob == null && !snapshot.cob.isNaN()) return RemoraCommandError.COB_MISMATCH + if (currentCob != null && snapshot.cob.isNaN()) return RemoraCommandError.COB_MISMATCH + + if ( + currentCob != null && + !snapshot.cob.isNaN() && + abs(currentCob - snapshot.cob) >= 20 + ) { + return RemoraCommandError.COB_MISMATCH + } + + val bolusIob = iobCobCalculator.calculateIobFromBolus().iob.toFloat() + val basalIob = iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().basaliob + val totalIob = bolusIob + basalIob + + if (abs(totalIob - snapshot.iob) >= hardLimits.maxBolus() / 10) return RemoraCommandError.IOB_MISMATCH + + val lastBg = lastBgData.lastBg() + val bg = lastBg?.let { it.smoothed ?: it.value } ?: Double.NaN + + if (!bg.isNaN()) { + if (snapshot.bg.isNaN()) return RemoraCommandError.BG_MISMATCH + if (abs(bg - snapshot.bg) >= 20) return RemoraCommandError.BG_MISMATCH + } + if (!bg.isNaN() && abs(bg - snapshot.bg) >= 20) return RemoraCommandError.IOB_MISMATCH + + val lastBolus = persistenceLayer.getNewestBolusOfType(BS.Type.NORMAL) + Duration.ZERO.absoluteValue + if ( + lastBolus != null && + (snapshot.lastBolusTime - Instant.fromEpochMilliseconds(lastBolus.timestamp)).absoluteValue >= 30.seconds && + abs(snapshot.lastBolusAmount - lastBolus.amount) > 0.1 + ) { + return RemoraCommandError.LAST_BOLUS_MISMATCH + } + + return null + } + + override suspend fun prepareTreatment(data: RemoraCommandData.Treatment): CommandHandler.Result { + val tt = data.temporaryTarget + when { + data.carbsAmount < 0f -> return wrapError(RemoraCommandError.INVALID_VALUE) + data.bolusAmount < 0f -> return wrapError(RemoraCommandError.INVALID_VALUE) + tt is RemoraCommandData.Treatment.TemporaryTarget.Set && + tt.duration <= Duration.ZERO -> return wrapError(RemoraCommandError.INVALID_VALUE) + + data.bolusAmount > 0f && data.timestamp == null -> when { + commandQueue.bolusInQueue() -> return wrapError(RemoraCommandError.BOLUS_IN_PROGRESS) + loop.runningMode.isSuspended() -> return wrapError(RemoraCommandError.PUMP_SUSPENDED) + } + } + + val constrainedBolusAmount = if (data.bolusAmount > 0f) { + constraintsChecker.applyBolusConstraints(ConstraintObject(data.bolusAmount.toDouble(), aapsLogger)).value().toFloat() + } else { + 0f + } + + val constrainedCarbsAmount = if (data.carbsAmount > 0f) { + constraintsChecker.applyCarbsConstraints(ConstraintObject(data.carbsAmount.roundToInt(), aapsLogger)).value().toFloat() + } else { + 0f + } + + var constrainedTempTarget = when (val tt = data.temporaryTarget) { + RemoraCommandData.Treatment.TemporaryTarget.Cancel -> tt + is RemoraCommandData.Treatment.TemporaryTarget.Set -> tt.copy( + target = tt.target.coerceIn(Constants.MIN_TT_MGDL.toFloat(), Constants.MAX_TT_MGDL.toFloat()), + duration = tt.duration.coerceAtMost(Constants.MAX_PROFILE_SWITCH_DURATION.minutes) + ) + + null -> null + } + + if (constrainedTempTarget is RemoraCommandData.Treatment.TemporaryTarget.Set && constrainedTempTarget.duration <= 0.minutes) + constrainedTempTarget = null + + if (constrainedTempTarget == null && constrainedBolusAmount == 0f && constrainedCarbsAmount == 0f) { + return wrapError(RemoraCommandError.INVALID_VALUE) + } + + smsCommunicator.get().invalidateMessage() + + return wrapSuccess( + data.copy( + temporaryTarget = constrainedTempTarget, + bolusAmount = constrainedBolusAmount, + carbsAmount = constrainedCarbsAmount + ) + ) + } + + override suspend fun CommandHandler.ExecutionScope.executeTreatment(data: RemoraCommandData.Treatment): CommandHandler.Result = coroutineScope { + val finalBolusAmount = if (data.bolusAmount > 0f && data.timestamp == null) { + when (val result = deliverBolus(data.bolusAmount)) { + is CommandHandler.Result.Error<*> -> return@coroutineScope wrapError(result.error) + is CommandHandler.Result.Success -> result.data + } + } else { + data.bolusAmount + } + + val timestamp = data.timestamp ?: Clock.System.now() + + val carbsJob = async { + if (data.carbsAmount > 0f) { + persistenceLayer.insertOrUpdateCarbs( + carbs = CA( + timestamp = (timestamp + data.carbsOffset).toEpochMilliseconds(), + amount = data.carbsAmount.toDouble(), + duration = data.carbsDuration.inWholeMilliseconds + ), + action = Action.CARBS, + source = Sources.Remora + ).await() + } + } + + val bolusJob = async { + if (data.bolusAmount > 0f && data.timestamp != null) { + persistenceLayer.insertOrUpdateBolus( + bolus = BS( + timestamp = timestamp.toEpochMilliseconds(), + amount = data.bolusAmount.toDouble(), + type = BS.Type.NORMAL + ), + action = Action.CARBS, + source = Sources.Remora + ).await() + } + } + + val ttJob = async { + when (val tt = data.temporaryTarget) { + RemoraCommandData.Treatment.TemporaryTarget.Cancel -> { + persistenceLayer.cancelCurrentTemporaryTargetIfAny( + timestamp = timestamp.toEpochMilliseconds(), + action = Action.TT, + source = Sources.Remora, + note = null, + listValues = listOf() + ).await() + } + + is RemoraCommandData.Treatment.TemporaryTarget.Set -> { + val reason = when (tt.ttType) { + RemoraCommandData.Treatment.TemporaryTargetType.EATING_SOON -> TT.Reason.EATING_SOON + RemoraCommandData.Treatment.TemporaryTargetType.ACTIVITY -> TT.Reason.ACTIVITY + RemoraCommandData.Treatment.TemporaryTargetType.HYPO -> TT.Reason.HYPOGLYCEMIA + RemoraCommandData.Treatment.TemporaryTargetType.CUSTOM -> TT.Reason.CUSTOM + } + persistenceLayer.insertAndCancelCurrentTemporaryTarget( + TT( + timestamp = timestamp.toEpochMilliseconds(), + duration = tt.duration.inWholeMilliseconds, + reason = reason, + lowTarget = tt.target.toDouble(), + highTarget = tt.target.toDouble() + ), + action = Action.TT, + source = Sources.Remora, + note = null, + listValues = listOf( + ValueWithUnit.Timestamp(timestamp.toEpochMilliseconds()), + ValueWithUnit.TETTReason(reason), + ValueWithUnit.fromGlucoseUnit(tt.target.toDouble(), GlucoseUnit.MGDL), + ValueWithUnit.Minute(tt.duration.inWholeMinutes.toInt()) + ) + ).await() + } + + null -> Unit + } + } + + carbsJob.await() + bolusJob.await() + ttJob.await() + + wrapSuccess(data.copy(bolusAmount = finalBolusAmount)) + } + + fun invalidateCommand() { + scope.launch { remoraLib.invalidateCurrentCommand() } + } + + private suspend fun CommandHandler.ExecutionScope.deliverBolus(amount: Float): CommandHandler.Result = coroutineScope { + if (amount <= 0f) return@coroutineScope wrapError(RemoraCommandError.INVALID_VALUE) + if (commandQueue.bolusInQueue()) return@coroutineScope wrapError(RemoraCommandError.BOLUS_IN_PROGRESS) + if (loop.runningMode.isSuspended()) return@coroutineScope wrapError(RemoraCommandError.PUMP_SUSPENDED) + val detailedBolusInfo = DetailedBolusInfo() + detailedBolusInfo.insulin = amount.toDouble() + val resultJob = async { + suspendCoroutine { + val callback = object : Callback() { + override fun run() { + it.resume(result) + } + } + commandQueue.bolus(detailedBolusInfo, callback) + } + } + var reportConnectionProgress = true + if (activePlugin.activePump.isConnected()) { + reportIntermediateProgress(RemoraCommand.Progress.Enqueued) + } + val progressJob = launch { + merge(pumpStatusProgressFlow, bolusProgressFlow) + .collect { event -> + when (event) { + is EventPumpStatusChanged -> when (event.status) { + EventPumpStatusChanged.Status.CONNECTING, + EventPumpStatusChanged.Status.HANDSHAKING, + -> { + if (reportConnectionProgress) + reportIntermediateProgress(RemoraCommand.Progress.Connecting(event.secondsElapsed)) + } + + EventPumpStatusChanged.Status.CONNECTED, + EventPumpStatusChanged.Status.PERFORMING, + -> { + if (reportConnectionProgress) + reportIntermediateProgress(RemoraCommand.Progress.Enqueued) + } + + else -> Unit + } + + is EventOverviewBolusProgress -> { + reportIntermediateProgress(RemoraCommand.Progress.Percentage(event.percent)) + reportConnectionProgress = false + } + } + } + } + val result = resultJob.await() + progressJob.cancel() + if (!result.success) { + if (!activePlugin.activePump.isConnected()) { + return@coroutineScope wrapError(RemoraCommandError.PUMP_TIMEOUT) + } + return@coroutineScope wrapError(RemoraCommandError.UNKNOWN) + } + wrapSuccess(result.bolusDelivered.toFloat()) + } +} \ No newline at end of file diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/StatusDataBuilder.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/StatusDataBuilder.kt new file mode 100644 index 000000000000..11a2e91496ed --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/remora/StatusDataBuilder.kt @@ -0,0 +1,558 @@ +package app.aaps.plugins.main.general.remora + +import app.aaps.core.data.model.BS +import app.aaps.core.data.model.GlucoseUnit +import app.aaps.core.data.model.RM +import app.aaps.core.data.model.SourceSensor +import app.aaps.core.data.model.TB +import app.aaps.core.data.model.TE +import app.aaps.core.data.model.TT +import app.aaps.core.data.model.TrendArrow +import app.aaps.core.data.time.T +import app.aaps.core.interfaces.aps.IobTotal +import app.aaps.core.interfaces.aps.Loop +import app.aaps.core.interfaces.configuration.Config +import app.aaps.core.interfaces.db.PersistenceLayer +import app.aaps.core.interfaces.iob.GlucoseStatusProvider +import app.aaps.core.interfaces.iob.IobCobCalculator +import app.aaps.core.interfaces.overview.LastBgData +import app.aaps.core.interfaces.plugin.ActivePlugin +import app.aaps.core.interfaces.profile.ProfileFunction +import app.aaps.core.interfaces.profile.ProfileUtil +import app.aaps.core.interfaces.receivers.ReceiverStatusStore +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.utils.HardLimits +import app.aaps.core.interfaces.utils.TrendCalculator +import app.aaps.core.keys.IntKey +import app.aaps.core.keys.UnitDoubleKey +import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.objects.extensions.combine +import app.aaps.core.objects.extensions.target +import app.aaps.core.objects.profile.ProfileSealed +import de.tebbeubben.remora.lib.model.status.RemoraStatusData +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.BasalDataPoint +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.BgReading +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.Bolus +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.BucketedDataPoint +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.CarbEntry +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.Deltas +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.DisplayBg +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.ExtendedBolus +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.Prediction +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.ProfileSwitch +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.TargetDataPoint +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.TemporaryBasal +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.TemporaryTarget +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.TemporaryTargetReason +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.TherapyEvent +import de.tebbeubben.remora.lib.model.status.RemoraStatusData.TherapyEventType +import org.joda.time.DateTimeZone +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlin.time.Clock +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@OptIn(ExperimentalTime::class) +class StatusDataBuilder @Inject constructor( + private val persistenceLayer: PersistenceLayer, + private val iobCobCalculator: IobCobCalculator, + private val activePlugin: ActivePlugin, + private val loop: Loop, + private val config: Config, + private val profileFunction: ProfileFunction, + private val rxBus: RxBus, + private val hardLimits: HardLimits, + private val rh: ResourceHelper, + private val lastBgData: LastBgData, + private val glucoseStatusProvider: GlucoseStatusProvider, + private val preferences: Preferences, + private val trendCalculator: TrendCalculator, + private val receiverStatusStore: ReceiverStatusStore, + private val profileUtil: ProfileUtil, +) { + + fun constructStatusData(): RemoraStatusData? { + val nowInstant = Clock.System.now() + val nowMillis = nowInstant.toEpochMilliseconds() + val endMillis = (nowInstant + 6.hours).toEpochMilliseconds() + val startMillis = (nowInstant - 24.hours - 2.minutes).toEpochMilliseconds() + + val profileSwitch = persistenceLayer.getEffectiveProfileSwitchActiveAt(nowMillis) ?: return null + val profile = profileFunction.getProfile(nowMillis) ?: return null + + val basalData = iobCobCalculator.getBasalData(profile, nowMillis) + + val tempTarget = persistenceLayer.getTemporaryTargetActiveAt(nowMillis) + + val activePump = activePlugin.activePump + + val shortStatus = RemoraStatusData.Short( + timestamp = nowInstant, + timezone = DateTimeZone.getDefault().id, + + cob = RemoraStatusData.Cob( + display = iobCobCalculator.getCobInfo("Remora").displayCob?.toFloat(), + futureCarbs = iobCobCalculator.getCobInfo("Remora").futureCarbs.toFloat(), + ), + + activeProfile = RemoraStatusData.ActiveProfile( + name = profileSwitch.originalProfileName, + percentage = profileSwitch.originalPercentage, + timeShift = profileSwitch.originalTimeshift.milliseconds.inWholeHours.toInt(), + start = Instant.fromEpochMilliseconds(profileSwitch.timestamp), + duration = if (profileSwitch.originalDuration != 0L) profileSwitch.originalDuration.milliseconds else null, + ), + + bgConfig = RemoraStatusData.BgConfig( + usesMgdl = profileFunction.getUnits() == GlucoseUnit.MGDL, + lowBgThreshold = profileUtil.convertToMgdl(preferences.get(UnitDoubleKey.OverviewLowMark), profileUtil.units).toFloat(), + highBgThreshold = profileUtil.convertToMgdl(preferences.get(UnitDoubleKey.OverviewHighMark), profileUtil.units).toFloat(), + ), + + displayBg = lastBgData.lastBg()?.let { bg -> + DisplayBg( + timestamp = Instant.fromEpochMilliseconds(bg.timestamp), + value = bg.value.toFloat(), + smoothedValue = bg.smoothed?.toFloat(), + trendArrow = convertAapsTrendToRemoraTrend(trendCalculator.getTrendArrow(iobCobCalculator.ads) ?: TrendArrow.NONE), + deltas = glucoseStatusProvider.glucoseStatusData?.let { status -> + Deltas( + delta = status.delta.toFloat(), + shortAverageDelta = status.shortAvgDelta.toFloat(), + longAverageDelta = status.longAvgDelta.toFloat() + ) + } + ) + }, + + iob = RemoraStatusData.Iob( + bolus = iobCobCalculator.calculateIobFromBolus().iob.toFloat(), + basal = iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().basaliob.toFloat(), + ), + + activeRunningMode = RemoraStatusData.ActiveRunningMode( + mode = convertAapsRmModeToRemoraMode(loop.runningMode), + start = Instant.fromEpochMilliseconds(loop.runningModeRecord.timestamp), + duration = if (loop.runningModeRecord.duration == 0L) null else loop.runningModeRecord.duration.milliseconds, + ), + + currentTarget = RemoraStatusData.CurrentTarget( + target = tempTarget?.target()?.toFloat() ?: profile.getTargetMgdl().toFloat(), + tempTargetStart = tempTarget?.timestamp?.let { Instant.fromEpochMilliseconds(it) }, + tempTargetDuration = tempTarget?.duration?.milliseconds, + ), + + deviceBattery = RemoraStatusData.DeviceBattery( + level = receiverStatusStore.batteryLevel, + isCharging = receiverStatusStore.isCharging + ), + + autosensRatio = iobCobCalculator.ads.getAutosensDataAtTime(nowMillis)?.autosensResult?.ratio?.toFloat() ?: 1f, + + basalStatus = RemoraStatusData.BasalStatus( + baseBasal = basalData.basal.toFloat(), + tempBasalAbsolute = if (basalData.isTempBasalRunning) basalData.tempBasalAbsolute.toFloat() else null, + ), + + reservoirLevel = activePump.reservoirLevel.let { level -> + if (level < 0) return@let null + val warnThreshold = preferences.get(IntKey.OverviewResWarning) + val criticalThreshold = preferences.get(IntKey.OverviewResCritical) + val isMax = level >= activePump.pumpDescription.maxResorvoirReading && activePump.pumpDescription.isPatchPump + RemoraStatusData.StatusLightElement(level.roundToInt(), warnThreshold, criticalThreshold, isMax) + }, + + reservoirChangedAt = persistenceLayer.getLastTherapyRecordUpToNow(TE.Type.INSULIN_CHANGE)?.timestamp?.let { changedAt -> + val warnThreshold = preferences.get(IntKey.OverviewIageWarning).hours + val criticalThreshold = preferences.get(IntKey.OverviewIageCritical).hours + RemoraStatusData.StatusLightElement(Instant.fromEpochMilliseconds(changedAt), warnThreshold, criticalThreshold) + }, + + sensorChangedAt = persistenceLayer.getLastTherapyRecordUpToNow(TE.Type.SENSOR_CHANGE)?.timestamp?.let { changedAt -> + val warnThreshold = preferences.get(IntKey.OverviewSageWarning).hours + val criticalThreshold = preferences.get(IntKey.OverviewSageCritical).hours + RemoraStatusData.StatusLightElement(Instant.fromEpochMilliseconds(changedAt), warnThreshold, criticalThreshold) + }, + + sensorBatteryLevel = activePlugin.activeBgSource.sensorBatteryLevel.let { level -> + if (level < 0) return@let null + val warnThreshold = preferences.get(IntKey.OverviewSbatWarning) + val criticalThreshold = preferences.get(IntKey.OverviewSbatCritical) + RemoraStatusData.StatusLightElement(level, warnThreshold, criticalThreshold) + }, + + pumpBatteryChangedAt = persistenceLayer.getLastTherapyRecordUpToNow(TE.Type.PUMP_BATTERY_CHANGE)?.timestamp?.let { changedAt -> + val warnThreshold = preferences.get(IntKey.OverviewBageWarning).hours + val criticalThreshold = preferences.get(IntKey.OverviewBageCritical).hours + RemoraStatusData.StatusLightElement(Instant.fromEpochMilliseconds(changedAt), warnThreshold, criticalThreshold) + }, + + pumpBatteryLevel = activePlugin.activePump.batteryLevel.let { level -> + if (level < 0) return@let null + val warnThreshold = preferences.get(IntKey.OverviewBattWarning) + val criticalThreshold = preferences.get(IntKey.OverviewBattCritical) + RemoraStatusData.StatusLightElement(level, warnThreshold, criticalThreshold) + }, + + cannulaChangedAt = persistenceLayer.getLastTherapyRecordUpToNow(TE.Type.CANNULA_CHANGE)?.timestamp?.let { changedAt -> + val warnThreshold = preferences.get(IntKey.OverviewCageWarning).hours + val criticalThreshold = preferences.get(IntKey.OverviewBageCritical).hours + RemoraStatusData.StatusLightElement(Instant.fromEpochMilliseconds(changedAt), warnThreshold, criticalThreshold) + }, + + usesPatchPump = activePump.pumpDescription.isPatchPump, + + lastBolus = persistenceLayer.getNewestBolusOfType(BS.Type.NORMAL)?.let { bolus -> + RemoraStatusData.LastBolus(Instant.fromEpochMilliseconds(bolus.timestamp), bolus.amount.toFloat()) + }, + + tempTargetTemplates = RemoraStatusData.TempTargetTemplates( + eatingSoon = RemoraStatusData.TempTargetTemplate( + target = profileUtil.convertToMgdl(preferences.get(UnitDoubleKey.OverviewEatingSoonTarget), profileUtil.units).toFloat(), + duration = preferences.get(IntKey.OverviewEatingSoonDuration).minutes + ), + hypo = RemoraStatusData.TempTargetTemplate( + target = profileUtil.convertToMgdl(preferences.get(UnitDoubleKey.OverviewHypoTarget), profileUtil.units).toFloat(), + duration = preferences.get(IntKey.OverviewHypoDuration).minutes + ), + activity = RemoraStatusData.TempTargetTemplate( + target = profileUtil.convertToMgdl(preferences.get(UnitDoubleKey.OverviewActivityTarget), profileUtil.units).toFloat(), + duration = preferences.get(IntKey.OverviewActivityDuration).minutes + ) + ) + ) + + return RemoraStatusData( + short = shortStatus, + isFakingTemps = activePlugin.activePump.isFakingTempsByExtendedBoluses, + bucketedData = getBucketedData(nowMillis, startMillis, endMillis), + basalData = getBasalData(startMillis, endMillis), + targetData = getTargetData(startMillis, endMillis), + predictions = getPredictions(), + bgReadings = getBgReadings(), + boluses = getBoluses(startMillis, endMillis), + carbs = getCarbs(startMillis, endMillis), + profileSwitches = getProfileSwitches(startMillis, endMillis), + therapyEvents = getTherapyEvents(startMillis, endMillis), + temporaryBasals = getTemporaryBasals(startMillis, endMillis), + temporaryTargets = getTemporaryTargets(startMillis, endMillis), + extendedBoluses = getExtendedBoluses(startMillis, endMillis), + profiles = getProfiles(), + runningModes = getRunningModeData(startMillis, endMillis) + ) + } + + private fun convertAapsRmModeToRemoraMode(mode: RM.Mode): RemoraStatusData.RunningMode = when (mode) { + RM.Mode.OPEN_LOOP -> RemoraStatusData.RunningMode.OPEN_LOOP + RM.Mode.CLOSED_LOOP -> RemoraStatusData.RunningMode.CLOSED_LOOP + RM.Mode.CLOSED_LOOP_LGS -> RemoraStatusData.RunningMode.CLOSED_LOOP_LGS + RM.Mode.DISABLED_LOOP -> RemoraStatusData.RunningMode.DISABLED_LOOP + RM.Mode.SUPER_BOLUS -> RemoraStatusData.RunningMode.SUPER_BOLUS + RM.Mode.DISCONNECTED_PUMP -> RemoraStatusData.RunningMode.DISCONNECTED_PUMP + RM.Mode.SUSPENDED_BY_PUMP -> RemoraStatusData.RunningMode.SUSPENDED_BY_PUMP + RM.Mode.SUSPENDED_BY_USER -> RemoraStatusData.RunningMode.SUSPENDED_BY_USER + RM.Mode.RESUME -> error("Invalid mode") + } + + private fun getRunningModeData(start: Long, end: Long): List = + generateSequence(start) { it + T.mins(5).msecs() } + .takeWhile { it <= end } + .map { time -> + val mode = persistenceLayer.getRunningModeActiveAt(time) + RemoraStatusData.RunningModeDataPoint( + timestamp = Instant.fromEpochMilliseconds(time), + runningMode = when (mode.mode) { + RM.Mode.OPEN_LOOP -> RemoraStatusData.RunningMode.OPEN_LOOP + RM.Mode.CLOSED_LOOP -> RemoraStatusData.RunningMode.CLOSED_LOOP + RM.Mode.CLOSED_LOOP_LGS -> RemoraStatusData.RunningMode.CLOSED_LOOP_LGS + RM.Mode.DISABLED_LOOP -> RemoraStatusData.RunningMode.DISABLED_LOOP + RM.Mode.SUPER_BOLUS -> RemoraStatusData.RunningMode.SUPER_BOLUS + RM.Mode.DISCONNECTED_PUMP -> RemoraStatusData.RunningMode.DISCONNECTED_PUMP + RM.Mode.SUSPENDED_BY_PUMP -> RemoraStatusData.RunningMode.SUSPENDED_BY_PUMP + RM.Mode.SUSPENDED_BY_USER -> RemoraStatusData.RunningMode.SUSPENDED_BY_USER + RM.Mode.RESUME -> error("Invalid mode") + } + ) + } + .fold(mutableListOf()) { acc, dp -> + if (acc.isEmpty() || acc.last().runningMode != dp.runningMode) acc += dp + acc + } + + private fun convertAapsTrendToRemoraTrend(aapsTrend: TrendArrow): RemoraStatusData.TrendArrow { + return when (aapsTrend) { + TrendArrow.NONE -> RemoraStatusData.TrendArrow.NONE + TrendArrow.TRIPLE_UP -> RemoraStatusData.TrendArrow.TRIPLE_UP + TrendArrow.DOUBLE_UP -> RemoraStatusData.TrendArrow.DOUBLE_UP + TrendArrow.SINGLE_UP -> RemoraStatusData.TrendArrow.SINGLE_UP + TrendArrow.FORTY_FIVE_UP -> RemoraStatusData.TrendArrow.FORTY_FIVE_UP + TrendArrow.FLAT -> RemoraStatusData.TrendArrow.FLAT + TrendArrow.FORTY_FIVE_DOWN -> RemoraStatusData.TrendArrow.FORTY_FIVE_DOWN + TrendArrow.SINGLE_DOWN -> RemoraStatusData.TrendArrow.SINGLE_DOWN + TrendArrow.DOUBLE_DOWN -> RemoraStatusData.TrendArrow.DOUBLE_DOWN + TrendArrow.TRIPLE_DOWN -> RemoraStatusData.TrendArrow.TRIPLE_DOWN + } + } + + private fun getProfiles(): List { + val profileStore = activePlugin.activeProfileSource.profile + return profileStore?.getProfileList()?.filter { profile -> + val profileToCheck = activePlugin.activeProfileSource.profile?.getSpecificProfile(profile.toString()) + profileToCheck != null && + ProfileSealed.Pure(profileToCheck, activePlugin) + .isValid("ProfileSwitch", activePlugin.activePump, config, rh, rxBus, hardLimits, false) + .isValid + }?.map { it.toString() } ?: emptyList() + } + + private fun TT.toModel() = TemporaryTarget( + timestamp = Instant.fromEpochMilliseconds(timestamp), + id = id, + duration = duration.milliseconds, + target = target().toFloat(), + reason = when (reason) { + TT.Reason.CUSTOM -> TemporaryTargetReason.CUSTOM + TT.Reason.HYPOGLYCEMIA -> TemporaryTargetReason.HYPOGLYCEMIA + TT.Reason.ACTIVITY -> TemporaryTargetReason.ACTIVITY + TT.Reason.EATING_SOON -> TemporaryTargetReason.EATING_SOON + TT.Reason.AUTOMATION -> TemporaryTargetReason.AUTOMATION + TT.Reason.WEAR -> TemporaryTargetReason.WEAR + } + ) + + private fun getTemporaryTargets(start: Long, end: Long): List = + persistenceLayer.getTemporaryTargetDataFromTimeToTime(start, end, true).blockingGet().map { it.toModel() } + + private fun TB.toModel() = TemporaryBasal( + timestamp = Instant.fromEpochMilliseconds(timestamp), + id = id, + isAbsolute = isAbsolute, + rate = rate.toFloat(), + duration = duration.milliseconds + ) + + private fun getTemporaryBasals(start: Long, end: Long): List = + persistenceLayer.getTemporaryBasalsActiveBetweenTimeAndTime(start, end).map { it.toModel() } + + private fun getTherapyEvents(start: Long, end: Long): List = + persistenceLayer.getTherapyEventDataFromToTime(start, end).blockingGet().map { te -> + TherapyEvent( + timestamp = Instant.fromEpochMilliseconds(te.timestamp), + id = te.id, + duration = te.duration.milliseconds, + type = when (te.type) { + TE.Type.NONE -> TherapyEventType.NONE + TE.Type.CANNULA_CHANGE -> TherapyEventType.CANNULA_CHANGE + TE.Type.INSULIN_CHANGE -> TherapyEventType.INSULIN_CHANGE + TE.Type.PUMP_BATTERY_CHANGE -> TherapyEventType.PUMP_BATTERY_CHANGE + TE.Type.SENSOR_CHANGE -> TherapyEventType.SENSOR_CHANGE + TE.Type.SENSOR_STARTED -> TherapyEventType.SENSOR_STARTED + TE.Type.SENSOR_STOPPED -> TherapyEventType.SENSOR_STOPPED + TE.Type.FINGER_STICK_BG_VALUE -> TherapyEventType.FINGER_STICK_BG_VALUE + TE.Type.EXERCISE -> TherapyEventType.EXERCISE + TE.Type.ANNOUNCEMENT -> TherapyEventType.ANNOUNCEMENT + TE.Type.QUESTION -> TherapyEventType.QUESTION + TE.Type.NOTE -> TherapyEventType.NOTE + TE.Type.APS_OFFLINE -> TherapyEventType.APS_OFFLINE + else -> TherapyEventType.NONE + }, + note = te.note, + glucose = te.glucose?.toFloat(), + glucoseType = when (te.glucoseType) { + TE.MeterType.FINGER -> RemoraStatusData.MeterType.FINGER + TE.MeterType.SENSOR -> RemoraStatusData.MeterType.SENSOR + TE.MeterType.MANUAL -> RemoraStatusData.MeterType.MANUAL + else -> null + }, + isMgdl = profileFunction.getUnits() == GlucoseUnit.MGDL + ) + } + + private fun getBucketedData(now: Long, start: Long, end: Long): List { + val existingBucketedData = iobCobCalculator.ads.bucketedData ?: emptyList() + + val existingBuckets = existingBucketedData.map { bucket -> + BucketedDataPoint( + timestamp = Instant.fromEpochMilliseconds(bucket.timestamp), + bgData = RemoraStatusData.BgData( + value = (bucket.smoothed ?: bucket.value).toFloat(), + filledGap = bucket.filledGap + ), + insulinData = getInsulinDataFor(bucket.timestamp), + autosensData = getAutosensDataFor(bucket.timestamp) + ) + } + val firstFutureBucketTime = existingBucketedData.maxByOrNull { it.timestamp }?.timestamp?.plus(T.mins(5).msecs()) ?: now + val missingFutureBucketTimestamps = generateSequence(firstFutureBucketTime) { it + T.mins(5).msecs() }.takeWhile { it <= end }.toList() + + val lastPastBucketTime = existingBucketedData.minByOrNull { it.timestamp }?.timestamp?.minus(T.mins(5).msecs()) ?: (now - T.mins(5).msecs()) + val missingPastBucketTimestamps = generateSequence(lastPastBucketTime) { it - T.mins(5).msecs() }.takeWhile { it >= start }.toList() + + val missingBuckets = (missingFutureBucketTimestamps + missingPastBucketTimestamps) + .map { time -> + BucketedDataPoint( + timestamp = Instant.fromEpochMilliseconds(time), + bgData = null, + insulinData = getInsulinDataFor(time), + autosensData = null + ) + } + + return (existingBuckets + missingBuckets) + } + + private fun getAutosensDataFor(time: Long) = + iobCobCalculator.ads.getAutosensDataAtTime(time)?.let { + RemoraStatusData.AutosensData( + ratio = it.autosensResult.ratio.toFloat(), + cob = it.cob.toFloat(), + carbsFromBolus = it.carbsFromBolus.toFloat(), + bgi = it.bgi.toFloat(), + deviation = it.deviation.toFloat(), + type = when (it.type) { + "", "non-meal" -> when (it.pastSensitivity) { + "C" -> RemoraStatusData.AutosensType.CSF + "+" -> RemoraStatusData.AutosensType.POSITIVE + "-" -> RemoraStatusData.AutosensType.NEGATIVE + else -> RemoraStatusData.AutosensType.NEUTRAL + } + + "uam" -> RemoraStatusData.AutosensType.UAM + "csf" -> RemoraStatusData.AutosensType.CSF + else -> RemoraStatusData.AutosensType.NEUTRAL + } + ) + } + + private fun getInsulinDataFor(time: Long) = + profileFunction.getProfile(time)?.let { profile -> + val iobTotal = iobCobCalculator.calculateFromTreatmentsAndTemps(time, profile) + val baseBasalIob = iobCobCalculator.calculateAbsoluteIobFromBaseBasals(time) + val absIob = IobTotal.combine(iobTotal, baseBasalIob).iob + RemoraStatusData.InsulinData( + iob = iobTotal.iob.toFloat(), + absoluteIob = absIob.toFloat(), + insulinActivity = iobTotal.activity.toFloat() + ) + } + + private fun getBasalData(start: Long, end: Long): List = + generateSequence(start) { it + T.mins(5).msecs() } + .takeWhile { it <= end } + .mapNotNull { time -> + val profile = profileFunction.getProfile(time) + if (profile == null) return@mapNotNull null + val basalData = iobCobCalculator.getBasalData(profile, time) + BasalDataPoint( + timestamp = Instant.fromEpochMilliseconds(time), + baselineBasal = basalData.basal.toFloat(), + tempBasalAbsolute = if (basalData.isTempBasalRunning) basalData.tempBasalAbsolute.toFloat() else null + ) + } + .fold(mutableListOf()) { acc, dp -> + if (acc.isEmpty() || + acc.last().baselineBasal != dp.baselineBasal || + acc.last().tempBasalAbsolute != dp.tempBasalAbsolute + ) acc += dp + acc + } + + private fun getTargetData(start: Long, end: Long): List { + val profile = profileFunction.getProfile() ?: return emptyList() + return generateSequence(start) { it + T.mins(1).msecs() } + .takeWhile { it <= end } + .map { time -> + val tt = persistenceLayer.getTemporaryTargetActiveAt(time) + TargetDataPoint( + timestamp = Instant.fromEpochMilliseconds(time), + target = (tt?.target() ?: ((profile.getTargetLowMgdl(time) + profile.getTargetHighMgdl(time)) / 2)).toFloat() + ) + } + .fold(mutableListOf()) { acc, dp -> + if (acc.isEmpty() || acc.last().target != dp.target) acc += dp + acc + } + } + + private fun getBgReadings(): List = + iobCobCalculator.ads.bgReadings.sortedBy { it.timestamp }.map { bgReading -> + BgReading( + timestamp = Instant.fromEpochMilliseconds(bgReading.timestamp), + id = bgReading.id, + value = bgReading.value.toFloat(), + trendArrow = convertAapsTrendToRemoraTrend(bgReading.trendArrow) + ) + } + + private fun getPredictions(): List { + val lastLoopRun = if (config.APS) loop.lastRun else null + val predictions = if (lastLoopRun?.request?.hasPredictions == true) lastLoopRun.constraintsProcessed?.predictionsAsGv?.sortedBy { it.timestamp } ?: emptyList() else emptyList() + return predictions.map { prediction -> + Prediction( + timestamp = Instant.fromEpochMilliseconds(prediction.timestamp), + value = prediction.value.toFloat(), + type = when (prediction.sourceSensor) { + SourceSensor.IOB_PREDICTION -> RemoraStatusData.PredictionType.IOB + SourceSensor.COB_PREDICTION -> RemoraStatusData.PredictionType.COB + SourceSensor.A_COB_PREDICTION -> RemoraStatusData.PredictionType.A_COB + SourceSensor.UAM_PREDICTION -> RemoraStatusData.PredictionType.UAM + SourceSensor.ZT_PREDICTION -> RemoraStatusData.PredictionType.ZT + else -> error("Invalid prediction type") + } + ) + } + } + + private fun getBoluses(start: Long, end: Long): List = + persistenceLayer.getBolusesFromTimeToTime(start, end, true).map { bolus -> + Bolus( + timestamp = Instant.fromEpochMilliseconds(bolus.timestamp), + id = bolus.id, + amount = bolus.amount.toFloat(), + type = when (bolus.type) { + BS.Type.NORMAL -> RemoraStatusData.BolusType.NORMAL + BS.Type.SMB -> RemoraStatusData.BolusType.SMB + BS.Type.PRIMING -> RemoraStatusData.BolusType.PRIMING + } + ) + } + + private fun getCarbs(start: Long, end: Long): List = + persistenceLayer.getCarbsFromTimeToTimeNotExpanded(start, end, true).map { carbs -> + CarbEntry( + timestamp = Instant.fromEpochMilliseconds(carbs.timestamp), + id = carbs.id, + amount = carbs.amount.toFloat(), + duration = carbs.duration.milliseconds + ) + } + + private fun getProfileSwitches(start: Long, end: Long) = + persistenceLayer.getEffectiveProfileSwitchesFromTimeToTime(start, end, true).map { profileSwitch -> + ProfileSwitch( + timestamp = Instant.fromEpochMilliseconds(profileSwitch.timestamp), + id = profileSwitch.id, + profileName = profileSwitch.originalProfileName, + timeshift = profileSwitch.originalTimeshift.milliseconds, + percentage = profileSwitch.originalPercentage, + duration = profileSwitch.originalDuration.milliseconds + ) + } + + private fun getExtendedBoluses(start: Long, end: Long): List = + persistenceLayer.getExtendedBolusesStartingFromTimeToTime(start, end, true) + .filter { it.duration != 0L } + .map { extendedBolus -> + ExtendedBolus( + timestamp = Instant.fromEpochMilliseconds(extendedBolus.timestamp), + id = extendedBolus.id, + amount = extendedBolus.amount.toFloat(), + duration = extendedBolus.duration.milliseconds + ) + } +} diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/smsCommunicator/SmsCommunicatorPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/smsCommunicator/SmsCommunicatorPlugin.kt index 339c08ef22c9..1fb166f99bd9 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/smsCommunicator/SmsCommunicatorPlugin.kt @@ -73,6 +73,7 @@ import app.aaps.core.validators.preferences.AdaptiveIntentPreference import app.aaps.core.validators.preferences.AdaptiveStringPreference import app.aaps.core.validators.preferences.AdaptiveSwitchPreference import app.aaps.plugins.main.R +import app.aaps.plugins.main.general.remora.RemoraPlugin import app.aaps.plugins.main.general.smsCommunicator.activities.SmsCommunicatorOtpActivity import app.aaps.plugins.main.general.smsCommunicator.events.EventSmsCommunicatorUpdateGui import app.aaps.plugins.main.general.smsCommunicator.otp.OneTimePassword @@ -117,7 +118,9 @@ class SmsCommunicatorPlugin @Inject constructor( private val glucoseStatusProvider: GlucoseStatusProvider, private val persistenceLayer: PersistenceLayer, private val decimalFormatter: DecimalFormatter, - private val configBuilder: ConfigBuilder + private val configBuilder: ConfigBuilder, + // TODO: Add interface + private val remora: dagger.Lazy ) : PluginBase( PluginDescription() .mainType(PluginType.GENERAL) @@ -133,6 +136,12 @@ class SmsCommunicatorPlugin @Inject constructor( private val disposable = CompositeDisposable() var allowedNumbers: MutableList = ArrayList() @Volatile var messageToConfirm: AuthRequest? = null + set(value) { + if (value != null) { + remora.get().invalidateCommand() + } + field = value + } @Volatile var lastRemoteBolusTime: Long = 0 override var messages = ArrayList() @@ -366,6 +375,10 @@ class SmsCommunicatorPlugin @Inject constructor( rxBus.send(EventSmsCommunicatorUpdateGui()) } + override fun invalidateMessage() { + messageToConfirm = null + } + private fun processBG(receivedSms: Sms) { val actualBG = iobCobCalculator.ads.actualBg() val lastBG = iobCobCalculator.ads.lastBg() diff --git a/plugins/main/src/main/res/layout/remora_fragment.xml b/plugins/main/src/main/res/layout/remora_fragment.xml new file mode 100644 index 000000000000..7684f4a1647b --- /dev/null +++ b/plugins/main/src/main/res/layout/remora_fragment.xml @@ -0,0 +1,19 @@ + + + +