diff --git a/.circleci/config.yml b/.circleci/config.yml
index b66ac53c714..4593de71fc5 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -5,7 +5,7 @@ version: 2.1
# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
orbs:
android: circleci/android@2.3.0
- codecov: codecov/codecov@3.2.4
+ codecov: codecov/codecov@3.3.0
jobs:
# Below is the definition of your job to build and test your app, you can rename and customize it as you want.
@@ -14,7 +14,7 @@ jobs:
executor:
name: android/android-machine
resource-class: large
- tag: 2023.07.1
+ tag: 2023.11.1
steps:
- checkout
@@ -34,11 +34,19 @@ jobs:
- android/run-tests:
test-command: ./gradlew --stacktrace jacocoAllDebugReport
- # And finally run the release build
- # - run:
- # name: Assemble release build
- # command: |
- # ./gradlew assembleRelease
+ - run:
+ name: Save test results
+ command: |
+ mkdir -p ~/test-results/junit/
+ find . -type f -regex ".*/build/outputs/androidTest-results/.*xml" -exec cp {} ~/test-results/junit/ \;
+ when: always
+
+ - store_test_results:
+ path: ~/test-results
+
+ - store_artifacts:
+ path: ~/test-results/junit
+
- codecov/upload:
file: './build/reports/jacoco/jacocoAllDebugReport/jacocoAllDebugReport.xml'
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 62973f00e86..9f97d7bb0a4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -207,6 +207,7 @@ android {
//Deleting it causes a binding error
buildFeatures {
dataBinding = true
+ buildConfig = true
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a7b77fb53b5..b20658fa065 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -97,6 +97,8 @@
+
+
diff --git a/app/src/main/kotlin/app/aaps/MainActivity.kt b/app/src/main/kotlin/app/aaps/MainActivity.kt
index 59150d15125..b4388f49bcf 100644
--- a/app/src/main/kotlin/app/aaps/MainActivity.kt
+++ b/app/src/main/kotlin/app/aaps/MainActivity.kt
@@ -277,7 +277,7 @@ class MainActivity : DaggerAppCompatActivityWithResult() {
})
// Setup views on 2nd and next activity start
// On 1st start app is still initializing, start() is delayed and run from EventAppInitialized
- if (config.appInitialized) start()
+ if (config.appInitialized) setupViews()
}
private fun start() {
diff --git a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt
index 48223fce397..54b17aceae4 100644
--- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt
+++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt
@@ -43,13 +43,14 @@ import app.aaps.plugins.source.GlunovoPlugin
import app.aaps.plugins.source.IntelligoPlugin
import app.aaps.plugins.source.MM640gPlugin
import app.aaps.plugins.source.NSClientSourcePlugin
+import app.aaps.plugins.source.PathedOTAppPlugin
import app.aaps.plugins.source.PathedSIAppPlugin
import app.aaps.plugins.source.PathedSinoAppPlugin
import app.aaps.plugins.source.PoctechPlugin
import app.aaps.plugins.source.RandomBgPlugin
import app.aaps.plugins.source.TomatoPlugin
import app.aaps.plugins.source.XdripSourcePlugin
-import app.aaps.plugins.sync.dataBroadcaster.DataBroadcastPlugin
+import app.aaps.plugins.sync.tizen.TizenPlugin
import app.aaps.plugins.sync.nsclient.NSClientPlugin
import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin
import app.aaps.plugins.sync.openhumans.OpenHumansUploaderPlugin
@@ -352,7 +353,7 @@ abstract class PluginsListModule {
@AllConfigs
@IntoMap
@IntKey(368)
- abstract fun bindDataBroadcastPlugin(plugin: DataBroadcastPlugin): PluginBase
+ abstract fun bindDataBroadcastPlugin(plugin: TizenPlugin): PluginBase
@Binds
@AllConfigs
@@ -432,6 +433,12 @@ abstract class PluginsListModule {
@IntKey(470)
abstract fun bindGlunovoPlugin(plugin: GlunovoPlugin): PluginBase
+ @Binds
+ @AllConfigs
+ @IntoMap
+ @IntKey(666)
+ abstract fun bindPatchedOTAppPlugin(plugin: PathedOTAppPlugin): PluginBase
+
@Binds
@AllConfigs
@IntoMap
diff --git a/app/src/main/kotlin/app/aaps/receivers/DataReceiver.kt b/app/src/main/kotlin/app/aaps/receivers/DataReceiver.kt
index f2b50acbcb2..ee5fc7c14a8 100644
--- a/app/src/main/kotlin/app/aaps/receivers/DataReceiver.kt
+++ b/app/src/main/kotlin/app/aaps/receivers/DataReceiver.kt
@@ -18,6 +18,7 @@ import app.aaps.plugins.source.DexcomPlugin
import app.aaps.plugins.source.EversensePlugin
import app.aaps.plugins.source.GlimpPlugin
import app.aaps.plugins.source.MM640gPlugin
+import app.aaps.plugins.source.PathedOTAppPlugin
import app.aaps.plugins.source.PathedSIAppPlugin
import app.aaps.plugins.source.PathedSinoAppPlugin
import app.aaps.plugins.source.PoctechPlugin
@@ -69,6 +70,12 @@ open class DataReceiver : DaggerBroadcastReceiver() {
it.copyString("collection", bundle)
it.copyString("data", bundle)
}.build()).build()
+ Intents.OTAPP_BG ->
+ OneTimeWorkRequest.Builder(PathedOTAppPlugin.PathedOTAppWorker::class.java)
+ .setInputData(Data.Builder().also {
+ it.copyString("collection", bundle)
+ it.copyString("data", bundle)
+ }.build()).build()
Intents.SIAPP_BG ->
OneTimeWorkRequest.Builder(PathedSIAppPlugin.PathedSIAppWorker::class.java)
.setInputData(Data.Builder().also {
diff --git a/build.gradle.kts b/build.gradle.kts
index d4f9346f94e..594abcd76bb 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,7 @@ buildscript {
maven("https://mirrors.cloud.tencent.com/nexus/repository/maven-public/")
}
dependencies {
- classpath("com.android.tools.build:gradle:8.1.3")
+ classpath("com.android.tools.build:gradle:8.2.1")
classpath("com.google.gms:google-services:4.4.0")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9")
@@ -22,7 +22,7 @@ buildscript {
}
plugins {
- id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
+ id("org.jlleitschuh.gradle.ktlint") version "12.0.3"
}
allprojects {
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 7cd2f0a6474..223da698c98 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -1,7 +1,7 @@
object KtsBuildVersions {
- const val gradle = "8.1.3"
- const val kotlin = "1.9.0"
+ const val gradle = "8.2.0"
+ const val kotlin = "1.9.10"
}
plugins {
diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt
index 719175d1a00..ecc70d9727b 100644
--- a/buildSrc/src/main/kotlin/Libs.kt
+++ b/buildSrc/src/main/kotlin/Libs.kt
@@ -152,8 +152,11 @@ object Libs {
}
object Mockito {
+ private const val mockitoVersion = "5.6.0"
- const val jupiter = "org.mockito:mockito-junit-jupiter:5.6.0"
+ const val android = "org.mockito:mockito-android:$mockitoVersion"
+ const val core = "org.mockito:mockito-core:$mockitoVersion"
+ const val jupiter = "org.mockito:mockito-junit-jupiter:$mockitoVersion"
const val kotlin = "org.mockito.kotlin:mockito-kotlin:5.1.0"
}
@@ -197,19 +200,5 @@ object Libs {
const val commonCodecs = "commons-codec:commons-codec:1.16.0"
const val kulid = "com.github.guepardoapps:kulid:2.0.0.0"
const val xstream = "com.thoughtworks.xstream:xstream:1.4.20"
-
- const val ormLite = "4.46"
-
- const val junit = "4.13.2"
- const val mockito = "5.6.0"
- const val dexmaker = "1.2"
- const val byteBuddy = "1.12.8"
-
- const val androidx_junit = "1.1.5"
- const val androidx_rules = "1.5.0"
-
- const val kotlinx_datetime = "0.4.1"
- const val kotlinx_serialization = "1.6.0"
-
- const val caverock_androidsvg = "1.4"
+ const val connectiqSdk = "com.garmin.connectiq:ciq-companion-app-sdk:2.0.3@aar"
}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt
index 80f83500752..340ef9ae8aa 100644
--- a/buildSrc/src/main/kotlin/Versions.kt
+++ b/buildSrc/src/main/kotlin/Versions.kt
@@ -2,7 +2,7 @@ import org.gradle.api.JavaVersion
object Versions {
- const val appVersion = "3.2.0.2"
+ const val appVersion = "3.2.0.4"
const val versionCode = 1500
const val ndkVersion = "21.1.6352462"
diff --git a/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts b/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts
index 22fc2175b7a..bd520b08635 100644
--- a/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts
+++ b/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts
@@ -22,6 +22,9 @@ dependencies {
androidTestImplementation(Libs.AndroidX.Test.rules)
androidTestImplementation(Libs.Google.truth)
androidTestImplementation(Libs.AndroidX.Test.uiAutomator)
+ androidTestImplementation(Libs.Mockito.core)
+ androidTestImplementation(Libs.Mockito.android)
+ androidTestImplementation(Libs.Mockito.kotlin)
}
tasks.withType {
diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt
index 5a887617e86..ad30c2b047e 100644
--- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt
+++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt
@@ -1,5 +1,6 @@
package app.aaps.core.interfaces.pump
+import app.aaps.core.interfaces.db.GlucoseUnit
import app.aaps.core.interfaces.profile.Profile
import app.aaps.core.interfaces.pump.defs.PumpType
import app.aaps.core.interfaces.utils.DateUtil
@@ -257,6 +258,26 @@ interface PumpSync {
**/
fun insertTherapyEventIfNewWithTimestamp(timestamp: Long, type: DetailedBolusInfo.EventType, note: String? = null, pumpId: Long? = null, pumpType: PumpType, pumpSerial: String): Boolean
+ /**
+ * Synchronization of FINGER_STICK_BG_VALUE events
+ *
+ * Assuming there will be no clash on timestamp from different pumps
+ * only timestamp and type is compared
+ *
+ * If db record doesn't exist, new record is created.
+ * If exists, data is ignored
+ *
+ * @param timestamp timestamp of event from pump history
+ * @param glucose glucose value
+ * @param glucoseUnit glucose unit
+ * @param note note
+ * @param pumpId pump id from history if available
+ * @param pumpType pump type like PumpType.ACCU_CHEK_COMBO
+ * @param pumpSerial pump serial number
+ * @return true if new record is created
+ **/
+ fun insertFingerBgIfNewWithTimestamp(timestamp: Long, glucose: Double, glucoseUnit: GlucoseUnit, note: String? = null, pumpId: Long? = null, pumpType: PumpType, pumpSerial: String): Boolean
+
/**
* Create an announcement
*
diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt
index be322b46efd..97fe32a5fb7 100644
--- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt
+++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt
@@ -25,6 +25,7 @@ interface Intents {
const val EXTRA_BG_SLOPE = "com.eveningoutpost.dexdrip.Extras.BgSlope"
const val EXTRA_BG_SLOPE_NAME = "com.eveningoutpost.dexdrip.Extras.BgSlopeName"
const val EXTRA_SENSOR_BATTERY = "com.eveningoutpost.dexdrip.Extras.SensorBattery"
+ const val EXTRA_SENSOR_STARTED_AT = "com.eveningoutpost.dexdrip.Extras.SensorStartedAt"
const val EXTRA_TIMESTAMP = "com.eveningoutpost.dexdrip.Extras.Time"
const val EXTRA_RAW = "com.eveningoutpost.dexdrip.Extras.Raw"
const val XDRIP_DATA_SOURCE_DESCRIPTION = "com.eveningoutpost.dexdrip.Extras.SourceDesc"
@@ -47,6 +48,9 @@ interface Intents {
var AIDEX_TRANSMITTER_SN = "com.microtechmd.cgms.aidex.TransmitterSerialNumber"
var AIDEX_SENSOR_ID = "com.microtechmd.cgms.aidex.SensorId"
+ // Patched Ottai App -> AAPS
+ const val OTAPP_BG = "cn.diyaps.sharing.OT_APP"
+
// Patched SI App -> AAPS
const val SIAPP_BG = "cn.diyaps.sharing.SI_APP"
diff --git a/core/ui/src/main/res/drawable/ic_user_options.xml b/core/ui/src/main/res/drawable/ic_user_options.xml
index c19c55f8bce..f65abd71393 100644
--- a/core/ui/src/main/res/drawable/ic_user_options.xml
+++ b/core/ui/src/main/res/drawable/ic_user_options.xml
@@ -5,8 +5,8 @@
android:viewportHeight="24">
+ android:fillColor="@color/userOption"/>
+ android:fillColor="@color/userOption"/>
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ottai_icon.png b/core/ui/src/main/res/mipmap-xhdpi/ottai_icon.png
new file mode 100644
index 00000000000..08ba082fe5d
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ottai_icon.png differ
diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt
index 3d36fa494a6..49b808f125e 100644
--- a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt
+++ b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt
@@ -1,22 +1,56 @@
package app.aaps.core.utils
-import org.joda.time.DateTime
+import java.time.Duration
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
-object MidnightUtils {
- /*
+/**
* Midnight time conversion
*/
+object MidnightUtils {
+
+ /**
+ * Actual passed seconds from midnight ignoring DST change
+ * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes)
+ *
+ * @return seconds
+ */
fun secondsFromMidnight(): Int {
- val passed = DateTime().millisOfDay.toLong()
- return (passed / 1000).toInt()
+ val nowZoned = ZonedDateTime.now()
+ val localTime = nowZoned.toLocalTime()
+ val midnight = nowZoned.toLocalDate().atStartOfDay(nowZoned.zone).toLocalTime()
+ val duration = Duration.between(midnight, localTime)
+ return duration.seconds.toInt()
}
- fun secondsFromMidnight(date: Long): Int {
- val passed = DateTime(date).millisOfDay.toLong()
- return (passed / 1000).toInt()
+ /**
+ * Passed seconds from midnight for specified time ignoring DST change
+ * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes)
+ *
+ * @param timestamp time
+ * @return seconds
+ */
+ fun secondsFromMidnight(timestamp: Long): Int {
+ val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
+ val localTime = timeZoned.toLocalTime()
+ val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime()
+ val duration: Duration = Duration.between(midnight, localTime)
+ return duration.seconds.toInt()
}
- fun milliSecFromMidnight(date: Long): Long {
- return DateTime(date).millisOfDay.toLong()
+ /**
+ * Passed milliseconds from midnight for specified time ignoring DST change
+ * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes)
+ *
+ * @param timestamp time
+ * @return milliseconds
+ */
+ fun milliSecFromMidnight(timestamp: Long): Long {
+ val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
+ val localTime = timeZoned.toLocalTime()
+ val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime()
+ val duration = Duration.between(midnight, localTime)
+ return duration.toMillis()
}
}
\ No newline at end of file
diff --git a/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt b/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt
new file mode 100644
index 00000000000..46d4b46faec
--- /dev/null
+++ b/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt
@@ -0,0 +1,67 @@
+package app.aaps.core.utils
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.util.TimeZone
+
+class MidnightUtilsTest {
+
+ @BeforeEach fun setUp() {
+ TimeZone.setDefault(TimeZone.getTimeZone("Europe/Amsterdam"))
+ }
+
+ @Test
+ fun secondsFromMidnight() {
+ val time = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.secondsFromMidnight(time)).isIn(0..24 * 3600)
+ }
+
+ @Test
+ fun testSecondsFromMidnight() {
+ val midnight = LocalDate.now().atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.secondsFromMidnight(midnight)).isEqualTo(0)
+ val oneHourAfter = LocalDateTime.ofInstant(Instant.ofEpochMilli(midnight), ZoneId.systemDefault()).atZone(ZoneId.systemDefault()).plusHours(1).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.secondsFromMidnight(oneHourAfter)).isEqualTo(3600)
+ }
+
+ @Test
+ fun milliSecFromMidnight() {
+ val midnight = LocalDate.now().atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.secondsFromMidnight(midnight)).isEqualTo(0)
+ val oneHourAfter = LocalDateTime.ofInstant(Instant.ofEpochMilli(midnight), ZoneId.systemDefault()).atZone(ZoneId.systemDefault()).plusHours(1).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.milliSecFromMidnight(oneHourAfter)).isEqualTo(3600 * 1000)
+ }
+
+ @Test fun testDateTimeToDuration() {
+ val dateTime = ZonedDateTime.of(1991, 8, 13, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101)
+ assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L)
+ }
+
+ @Test fun testDateTimeToDurationAtDstChange() {
+ val dateTime = ZonedDateTime.of(2020, 10, 25, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101)
+ assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L)
+ }
+
+ @Test fun testDateTimeToDurationAtDstReverseChange() {
+ val dateTime = ZonedDateTime.of(2020, 3, 29, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101)
+ assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L)
+ }
+
+ @Test fun testDateTimeInOtherZone() {
+ TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"))
+ assertThat(ZoneId.systemDefault().id).isEqualTo("America/Los_Angeles")
+ val dateTime = ZonedDateTime.of(2020, 3, 29, 23, 5, 1, 0, ZoneId.of("America/Los_Angeles")).toInstant().toEpochMilli()
+ assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101)
+ assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L)
+ }
+}
\ No newline at end of file
diff --git a/database/entities/src/main/kotlin/app/aaps/database/entities/GlucoseValue.kt b/database/entities/src/main/kotlin/app/aaps/database/entities/GlucoseValue.kt
index 81e8529fc09..9348196921b 100644
--- a/database/entities/src/main/kotlin/app/aaps/database/entities/GlucoseValue.kt
+++ b/database/entities/src/main/kotlin/app/aaps/database/entities/GlucoseValue.kt
@@ -97,6 +97,7 @@ data class GlucoseValue(
DEXCOM_G6_NATIVE_XDRIP("G6 Native"),
DEXCOM_G5_NATIVE_XDRIP("G5 Native"),
DEXCOM_G6_G5_NATIVE_XDRIP("G6 Native / G5 Native"),
+ DEXCOM_G7_NATIVE_XDRIP("G7"),
LIBRE_1_OTHER("Other App"),
LIBRE_1_NET("Network libre"),
LIBRE_1_BLUE("BlueReader"),
@@ -115,6 +116,7 @@ data class GlucoseValue(
MM_600_SERIES("MM600Series"),
EVERSENSE("Eversense"),
AIDEX("GlucoRx Aidex"),
+ OTApp("Ottai App"),
SIApp("SI App"),
SinoApp("SI App"),
RANDOM("Random"),
diff --git a/gradle.properties b/gradle.properties
index 743829c702a..8e287024457 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,7 +17,7 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true
org.gradle.warning.mode=all
-org.gradle.jvmargs=-Xmx3g -XX:+UseParallelGC
+org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
android.enableJetifier=false
android.useAndroidX=true
@@ -32,5 +32,4 @@ android.nonTransitiveRClass=true
# null: KtCallExpression
# https://youtrack.jetbrains.com/issue/KT-58027
kapt.use.jvm.ir=false
-android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=true
diff --git a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt
index 1db60cde05b..bac571d2976 100644
--- a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt
+++ b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt
@@ -1,5 +1,6 @@
package app.aaps.implementation.pump
+import app.aaps.core.interfaces.db.GlucoseUnit
import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.logging.UserEntryLogger
@@ -16,6 +17,7 @@ import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.core.interfaces.utils.DateUtil
import app.aaps.core.interfaces.utils.T
import app.aaps.core.main.events.EventNewNotification
+import app.aaps.core.main.extensions.fromConstant
import app.aaps.core.main.pump.fromDbPumpType
import app.aaps.core.main.pump.toDbPumpType
import app.aaps.core.main.pump.toDbSource
@@ -291,6 +293,42 @@ class PumpSyncImplementation @Inject constructor(
}
}
+ override fun insertFingerBgIfNewWithTimestamp(timestamp: Long, glucose: Double, glucoseUnit: GlucoseUnit, note: String?, pumpId: Long?, pumpType: PumpType, pumpSerial: String): Boolean {
+ if (!confirmActivePump(timestamp, pumpType, pumpSerial)) return false
+ var type = TherapyEvent.Type.FINGER_STICK_BG_VALUE
+ val therapyEvent = TherapyEvent(
+ timestamp = timestamp,
+ type = type,
+ duration = 0,
+ note = note,
+ enteredBy = "AndroidAPS",
+ glucose = glucose,
+ glucoseType = TherapyEvent.MeterType.FINGER,
+ glucoseUnit = TherapyEvent.GlucoseUnit.fromConstant(glucoseUnit),
+ interfaceIDs_backing = InterfaceIDs(
+ pumpId = pumpId,
+ pumpType = pumpType.toDbPumpType(),
+ pumpSerial = pumpSerial
+ )
+ )
+ uel.log(
+ action = UserEntry.Action.CAREPORTAL,
+ source = pumpType.source.toDbSource(),
+ note = note,
+ timestamp = timestamp,
+ ValueWithUnit.Timestamp(timestamp), ValueWithUnit.TherapyEventType(type)
+ )
+ repository.runTransactionForResult(InsertIfNewByTimestampTherapyEventTransaction(therapyEvent))
+ .doOnError {
+ aapsLogger.error(LTag.DATABASE, "Error while saving TherapyEvent", it)
+ }
+ .blockingGet()
+ .also { result ->
+ result.inserted.forEach { aapsLogger.debug(LTag.DATABASE, "Inserted TherapyEvent $it") }
+ return result.inserted.size > 0
+ }
+ }
+
override fun insertAnnouncement(error: String, pumpId: Long?, pumpType: PumpType, pumpSerial: String) {
if (!confirmActivePump(dateUtil.now(), pumpType, pumpSerial)) return
disposable += repository.runTransaction(InsertTherapyEventAnnouncementTransaction(error, pumpId, pumpType.toDbPumpType(), pumpSerial))
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 9f41eacf241..1bf97596c81 100644
--- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt
+++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt
@@ -119,13 +119,13 @@ class CommandQueueImplementation @Inject constructor(
return@subscribe
}
aapsLogger.debug(LTag.PROFILE, "onEventProfileSwitchChanged")
- val effective = repository.getEffectiveProfileSwitchActiveAt(dateUtil.now()).blockingGet()
profileFunction.getRequestedProfile()?.let {
setProfile(ProfileSealed.PS(it), it.interfaceIDs.nightscoutId != null, object : Callback() {
override fun run() {
if (!result.success) {
uiInteraction.runAlarm(result.comment, rh.gs(app.aaps.core.ui.R.string.failed_update_basal_profile), app.aaps.core.ui.R.raw.boluserror)
- } else if (result.enacted || effective is ValueWrapper.Existing && effective.value.originalEnd < dateUtil.now() && effective.value.originalDuration != 0L) {
+ } else /* if (result.enacted || effective is ValueWrapper.Existing && effective.value.originalEnd < dateUtil.now() && effective.value.originalDuration != 0L) */ {
+ // Pump may return enacted == false if basal profile is the same, but IC/ISF can be different
val nonCustomized = ProfileSealed.PS(it).convertToNonCustomizedProfile(dateUtil)
EffectiveProfileSwitch(
timestamp = dateUtil.now(),
diff --git a/insight/src/main/res/values-ru-rRU/alert_titles.xml b/insight/src/main/res/values-ru-rRU/alert_titles.xml
index 4cb9822a47b..9ae65db2bda 100644
--- a/insight/src/main/res/values-ru-rRU/alert_titles.xml
+++ b/insight/src/main/res/values-ru-rRU/alert_titles.xml
@@ -1,7 +1,7 @@
- Дать болюс
- Недоставленный болюс
+ Ввести болюс
+ Несостоявшийся болюс
Будильник
Замените инфузионный набор
TBR завершен
diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt
index fa6cd4cdd56..a27f79c4b63 100644
--- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt
+++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt
@@ -79,6 +79,7 @@ import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.json.JSONObject
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.abs
@@ -135,6 +136,8 @@ class LoopPlugin @Inject constructor(
disposable += rxBus
.toObservable(EventTempTargetChange::class.java)
.observeOn(aapsSchedulers.io)
+ // Skip db change of ending previous TT
+ .debounce(10L, TimeUnit.SECONDS)
.subscribe({ invoke("EventTempTargetChange", true) }, fabricPrivacy::logException)
}
diff --git a/plugins/aps/src/main/res/values-es-rES/strings.xml b/plugins/aps/src/main/res/values-es-rES/strings.xml
index b9b3566744e..f46d421cb7c 100644
--- a/plugins/aps/src/main/res/values-es-rES/strings.xml
+++ b/plugins/aps/src/main/res/values-es-rES/strings.xml
@@ -1,7 +1,7 @@
Habilitar la relación de sensibilidad basada en TDD para modificar las basales y el objetivo de glucosa
- Utiliza las últimas 24h TDD/7D TDD para calcular el ratio de sensibilidad utilizado para aumentar o disminuir la tasa basal, y también ajustar el objetivo de glucosa si estas opciones están activadas, de la misma forma que lo hace Autosens. Se recomienda comenzar con esta opción desactivada
+ Utiliza las últimas 24h TDD/7D TDD para calcular el factor de sensibilidad utilizado para aumentar o disminuir la tasa basal, y también ajusta el objetivo de glucosa si estas opciones están activadas, de la misma forma que lo hace Autosens. Se recomienda comenzar con esta opción desactivada
Factor de ajuste de ISF Dinámico %
Factor de ajuste para ISF Dinámico. Establezca más de 100% para una corrección más agresiva, y menos de 100% para correcciones más susves.
Objetivo temporal alto aumenta la sensibilidad
diff --git a/plugins/aps/src/main/res/values-fr-rFR/strings.xml b/plugins/aps/src/main/res/values-fr-rFR/strings.xml
index f4eebd48730..93cb120907c 100644
--- a/plugins/aps/src/main/res/values-fr-rFR/strings.xml
+++ b/plugins/aps/src/main/res/values-fr-rFR/strings.xml
@@ -1,5 +1,7 @@
+ Activer le ratio de sensibilité basé sur DTQ pour la modification du basal et la cible des glycémies
+ Utilise le dernier DTQ 24h/DTQ 7j pour calculer le ratio de sensibilité utilisé pour augmenter ou diminuer le taux de basal et ajuster aussi la cible de glycémie si ces options sont activées, de la même manière que Autosens. Il est recommandé de commencer avec cette option désactivée
Facteur d\'ajustement Si dynamique %
Facteur d\'ajustement pour Si dynamique. Définissez plus de 100 % pour des corrections plus agressives et moins de 100 % pour des corrections moins agressives.
Cible temp. haute élève la sensibilité
diff --git a/plugins/aps/src/main/res/values-iw-rIL/strings.xml b/plugins/aps/src/main/res/values-iw-rIL/strings.xml
index 9e5a0e2956f..f38db2d5d03 100644
--- a/plugins/aps/src/main/res/values-iw-rIL/strings.xml
+++ b/plugins/aps/src/main/res/values-iw-rIL/strings.xml
@@ -1,5 +1,7 @@
+ אפשר יחס רגישות המבוסס על המינון היומי הכולל לשינוי מינון בזאלי וערכי מטרה
+ השתמש במינון יומי כולל של 24 שעות\\7 ימים האחרונים כדי לחשב יחס רגישות לצורך העלאה או הורדה של המינון הבזאלי ובנוסף להתאים את ערך המטרה אם אפשרויות אלו מופעלותת באופן דומה ל-Autosense. מומלץ להתחיל כשאפשרות זו כבויה
כיוונון פקטור הרגישות הדינאמית %
כיוונון פקטור הרגישות הדינאמית. הגדירו יותר מ-100% לקבלת תיקונים אגרסיביים יותר ופחות מ-100% לקבלת תיקונים עדינים יותר.
ערך מטרה זמני גבוה מעלה את הרגישות
diff --git a/plugins/aps/src/main/res/values-nl-rNL/strings.xml b/plugins/aps/src/main/res/values-nl-rNL/strings.xml
index 1b232896730..7530c622972 100644
--- a/plugins/aps/src/main/res/values-nl-rNL/strings.xml
+++ b/plugins/aps/src/main/res/values-nl-rNL/strings.xml
@@ -1,5 +1,7 @@
+ Inschakelen van TDD gebaseerde gevoeligheid ratio voor basaal en glucose doel aanpassingen
+ Gebruikt de laatste 24u TDD/7D om de gevoeligheid te berekenen voor het verhogen of verlagen van de basaalstand en past het glucosedoel aan als deze opties zijn ingeschakeld, op dezelfde manier als Autosens dat doet. Advies is om te starten met deze optie uitgeschakeld
Dynamische Isf aanpassingsfactor %
Aanpassingsfactor voor DynamicISF. Stel meer dan 100% in voor een agressievere correctie en minder dan 100% voor minder agressieve correctie.
Hoog tijdelijk streefdoel verhoogt gevoeligheid
diff --git a/plugins/aps/src/main/res/values-ro-rRO/strings.xml b/plugins/aps/src/main/res/values-ro-rRO/strings.xml
index 51754ad6c41..c3f58255cc5 100644
--- a/plugins/aps/src/main/res/values-ro-rRO/strings.xml
+++ b/plugins/aps/src/main/res/values-ro-rRO/strings.xml
@@ -1,5 +1,7 @@
+ Activează raportul de sensibilitate bazat pe TDD pentru modificarea bazalei și a țintei glicemice
+ Folosește ultimele 24 de ore TDD/7D TDD pentru a calcula raportul de sensibilitate utilizat pentru creșterea sau scăderea ratei bazale, şi de asemenea ajustează ţinta glicemic[ dacă aceste opţiuni sunt activate, în acelaşi mod în care procedează Autosens. Este recomandat ca opțiunea să fie oprită la început
Factor de ajustare dinamic %
Factorul de ajustare pentru DynamicISF. Setează mai mult de 100% pentru doze de corecție mai agresive și mai puțin de 100% pentru corecții mai puțin agresive.
Ținte temporare mai mari cresc sensibilitatea
diff --git a/plugins/aps/src/main/res/values-ru-rRU/strings.xml b/plugins/aps/src/main/res/values-ru-rRU/strings.xml
index 9b405486f5b..69ec53722af 100644
--- a/plugins/aps/src/main/res/values-ru-rRU/strings.xml
+++ b/plugins/aps/src/main/res/values-ru-rRU/strings.xml
@@ -10,9 +10,9 @@
= 100]]>
OpenAPS СМБ
Динамический ISF
- Как часто СМБ будут подаваться в минутах
- Сопротивляемость понижает цель
- При обнаружении сопротивляемости целевой уровень гликемии понижается
+ Как часто SMB будут подаваться в минутах
+ Резистентность понижает цель
+ При обнаружении резистентности целевой уровень гликемии понижается
Чувствительность поднимает цель
При обнаружении чувствительности целевой уровень глюкозы повышается
модуль не активен
diff --git a/plugins/aps/src/main/res/values-sk-rSK/strings.xml b/plugins/aps/src/main/res/values-sk-rSK/strings.xml
index 606aceb1d2a..035f543be6c 100644
--- a/plugins/aps/src/main/res/values-sk-rSK/strings.xml
+++ b/plugins/aps/src/main/res/values-sk-rSK/strings.xml
@@ -1,5 +1,7 @@
+ Povoliť, aby citlivosť založená na CDD mohla upravovať bazál a cieľovú glykémiu
+ K výpočtu citlivosti použitej pre zvýšenie, alebo zníženie bazálu sa používa CDD za posledných 24 h / 7 D a tiež upravuje cieľovú glykémiu, pokiaľ je táto možnosť povolená, rovnako, ako to robí Autosens. Túto možnosť je doporučené na začiatok vypnúť.
Korekčný faktor pre Dynamickú ISF v %
Korekčný faktor pre dynamickú ISF. Pre agresivnejšie korekčné dávky nastavte hodnoty väčšie ako 100%, pre menej agresívne korekcie, menšie než 100%.
Vysoký dočasný cieľ zvýši citlivosť
diff --git a/plugins/configuration/src/main/res/values-ru-rRU/strings.xml b/plugins/configuration/src/main/res/values-ru-rRU/strings.xml
index e27f2817c2f..4421a56416b 100644
--- a/plugins/configuration/src/main/res/values-ru-rRU/strings.xml
+++ b/plugins/configuration/src/main/res/values-ru-rRU/strings.xml
@@ -35,7 +35,7 @@
Напоминание: новые профили инсулина требуют как минимум 5 часов длительности действия DIA. 5-6 часовая DIA эквивалентна трехчасовой на старых профилях.
Профиль
Переключить профиль
- Пропустить Мастер установки
+ Пропустить Мастер настройки
Нажмите на кнопку ниже, чтобы AAPS мог предложить/внести изменения в базал
Плагин чувствительности Sensitivity применяется для определения чувствительности к инсулину и вычисления активных углеводов COB. Дополнительная информация:
https://wiki.aaps.app/en/latest/Configuration/Sensitivity-detection-and-COB.html
diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt
index de331c6bd52..69dc86b7527 100644
--- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt
+++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt
@@ -1,9 +1,11 @@
package app.aaps.plugins.constraints.versionChecker
-import org.joda.time.LocalDate
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
class AllowedVersions {
@@ -38,7 +40,8 @@ class AllowedVersions {
fun endDateToMilliseconds(endDate: String): Long? {
try {
val dateTime = LocalDate.parse(endDate)
- return dateTime.toDate().time
+ val instant = dateTime.atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant()
+ return instant.toEpochMilli()
} catch (ignored: Exception) {
}
return null
diff --git a/plugins/constraints/src/main/res/values-nb-rNO/strings.xml b/plugins/constraints/src/main/res/values-nb-rNO/strings.xml
index 7e56ad8011f..510add70ed3 100644
--- a/plugins/constraints/src/main/res/values-nb-rNO/strings.xml
+++ b/plugins/constraints/src/main/res/values-nb-rNO/strings.xml
@@ -16,7 +16,7 @@
Versjon %1$s utløper den %2$s
- Bruker rekalkulerte data
+ Rekalkulerte data benyttes fordi noe BS-data mangler eller at BS ankommer på et ikke-forventet tidspunkt. Vil nullstille seg etter ca 24 timer. Ingen handling kreves
BS for nær:\n%1$s\n%2$s
beregnet på nytt
doble registreringer
diff --git a/plugins/constraints/src/main/res/values-pl-rPL/objectives.xml b/plugins/constraints/src/main/res/values-pl-rPL/objectives.xml
index 09bff75cad6..404b9e0b07d 100644
--- a/plugins/constraints/src/main/res/values-pl-rPL/objectives.xml
+++ b/plugins/constraints/src/main/res/values-pl-rPL/objectives.xml
@@ -2,7 +2,7 @@
Start
Zweryfikuj
- %1$d. Cel
+ Zadanie %1$d.
Zadanie %1$d nierozpoczęte
Zadanie %1$d nieukończone
Konfiguracja wizualizacji i monitorowania, analiza dawek bazowych i współczynników
diff --git a/plugins/constraints/src/main/res/values-ru-rRU/objectives.xml b/plugins/constraints/src/main/res/values-ru-rRU/objectives.xml
index bf60945ad5b..901b20b971c 100644
--- a/plugins/constraints/src/main/res/values-ru-rRU/objectives.xml
+++ b/plugins/constraints/src/main/res/values-ru-rRU/objectives.xml
@@ -12,7 +12,7 @@
Используйте режим Открытого цикла на протяжении нескольких дней и вручную выставляйте временные цели. Настройте ВЦ по умолчанию (ВЦ для Нагрузки, Гипо, Ожидания приема пищи) и используйте их.
Открытый цикл может быть использован для получения рекомендаций по терапии, если у вас нет совместимой помпы или если вы не готовы закрыть цикл.
Глубже понимаем работу открытого цикла, включая рекомендации по ВБС
- На основе накопленного опыта, определяем максимальную величину базала и задаем ее в помпе и в настройки AndroidAPS
+ На основе накопленного опыта, определяем максимальную величину базала и задаем ее в помпе и в настройках AAPS
Примите меры предосторожности и корректируйте, если необходимо, параметры безопасности.
Начинаем замыкать цикл с прекращением подачи инсулина при низких значениях ГК (режим Low Glucose Suspend)
Работа в замкнутом цикле с maxIOB = 0 на протяжении нескольких дней. Старайтесь избегать низкой ГК, чтобы не вызывать события приостановки подачи инсулина на низких сахарах.
diff --git a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt
index 0d8f29b5ad3..5e182063292 100644
--- a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt
+++ b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt
@@ -1,11 +1,12 @@
package app.aaps.plugins.constraints.versionChecker
import com.google.common.truth.Truth.assertThat
-import app.aaps.plugins.constraints.versionChecker.AllowedVersions
-import org.joda.time.LocalDate
import org.json.JSONArray
import org.json.JSONObject
import org.junit.jupiter.api.Test
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
class AllowedVersionsTest {
@@ -71,10 +72,10 @@ class AllowedVersionsTest {
@Test
fun endDateToMilliseconds() {
val definition = generateSupportedVersions()
- val endDate = AllowedVersions().endDateToMilliseconds(AllowedVersions().findByVersion(definition, "2.9.0-beta1")?.getString("endDate") ?: "1000/01/01")
- val dateTime = LocalDate(endDate)
+ val endDate = AllowedVersions().endDateToMilliseconds(AllowedVersions().findByVersion(definition, "2.9.0-beta1")?.getString("endDate") ?: "1000/01/01") ?: 0L
+ val dateTime = LocalDate.ofInstant(Instant.ofEpochMilli(endDate), ZoneId.systemDefault())
assertThat(dateTime.year).isEqualTo(2021)
- assertThat(dateTime.monthOfYear).isEqualTo(11)
+ assertThat(dateTime.monthValue).isEqualTo(11)
assertThat(dateTime.dayOfMonth).isEqualTo(7)
assertThat(AllowedVersions().endDateToMilliseconds("abdef")).isNull()
diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/overview/OverviewFragment.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/overview/OverviewFragment.kt
index 382741bc699..70cd3c921e7 100644
--- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/overview/OverviewFragment.kt
+++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/overview/OverviewFragment.kt
@@ -618,11 +618,14 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList
}
it.setCompoundDrawablesWithIntrinsicBounds(null, rh.gd(app.aaps.core.ui.R.drawable.ic_user_options), null, null)
it.text = event.title
-
it.setOnClickListener {
OKDialog.showConfirmation(context, rh.gs(R.string.run_question, event.title), { handler.post { automation.processEvent(event) } })
}
binding.buttonsLayout.userButtonsLayout.addView(it)
+ for (drawable in it.compoundDrawables ) {
+ drawable?.mutate()
+ drawable?.colorFilter = PorterDuffColorFilter(rh.gac(context, app.aaps.core.ui.R.attr.userOptionColor), PorterDuff.Mode.SRC_IN)
+ }
}
}
binding.buttonsLayout.userButtonsLayout.visibility = events.isNotEmpty().toVisibility()
diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt
index e9f46e8f651..23f27dddbb9 100644
--- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt
+++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt
@@ -124,7 +124,7 @@ class PersistentNotificationPlugin @Inject constructor(
val lastBG = iobCobCalculator.ads.lastBg()
val glucoseStatus = glucoseStatusProvider.glucoseStatusData
if (lastBG != null) {
- line1aa = profileUtil.fromMgdlToStringInUnits(lastBG.value)
+ line1aa = profileUtil.fromMgdlToStringInUnits(lastBG.recalculated)
line1 = line1aa
if (glucoseStatus != null) {
line1 += (" Δ" + profileUtil.fromMgdlToSignedStringInUnits(glucoseStatus.delta)
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 67bd7898039..9726e1bd4a2 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
@@ -356,11 +356,11 @@ class SmsCommunicatorPlugin @Inject constructor(
var reply = ""
val units = profileUtil.units
if (actualBG != null) {
- reply = rh.gs(R.string.sms_actual_bg) + " " + profileUtil.fromMgdlToStringInUnits(actualBG.value) + ", "
+ reply = rh.gs(R.string.sms_actual_bg) + " " + profileUtil.fromMgdlToStringInUnits(actualBG.recalculated) + ", "
} else if (lastBG != null) {
val agoMilliseconds = dateUtil.now() - lastBG.timestamp
val agoMin = (agoMilliseconds / 60.0 / 1000.0).toInt()
- reply = rh.gs(R.string.sms_last_bg) + " " + profileUtil.valueInCurrentUnitsDetect(lastBG.value) + " " + rh.gs(R.string.sms_min_ago, agoMin) + ", "
+ reply = rh.gs(R.string.sms_last_bg) + " " + profileUtil.valueInCurrentUnitsDetect(lastBG.recalculated) + " " + rh.gs(R.string.sms_min_ago, agoMin) + ", "
}
val glucoseStatus = glucoseStatusProvider.glucoseStatusData
if (glucoseStatus != null) reply += rh.gs(R.string.sms_delta) + " " + profileUtil.fromMgdlToUnits(glucoseStatus.delta) + " " + units + ", "
diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt
index 5eb7f3d41da..0415399cc43 100644
--- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt
+++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt
@@ -300,7 +300,7 @@ class ProfileFragment : DaggerFragment() {
binding.profileRemove.setOnClickListener {
activity?.let { activity ->
- OKDialog.showConfirmation(activity, rh.gs(R.string.delete_current_profile), {
+ OKDialog.showConfirmation(activity, rh.gs(R.string.delete_current_profile, profilePlugin.currentProfile()?.name), {
uel.log(
UserEntry.Action.PROFILE_REMOVED, UserEntry.Sources.LocalProfile, ValueWithUnit.SimpleString(
profilePlugin.currentProfile()?.name
diff --git a/plugins/main/src/main/res/values-nb-rNO/strings.xml b/plugins/main/src/main/res/values-nb-rNO/strings.xml
index f12a2a69a25..730cb981708 100644
--- a/plugins/main/src/main/res/values-nb-rNO/strings.xml
+++ b/plugins/main/src/main/res/values-nb-rNO/strings.xml
@@ -211,7 +211,7 @@
Endre dine inndata!
OpenAPS
Opplaster-batteri
- BS data status
+ Status BS-data
Innstillinger for hurtigknapp
Hold skjermen påslått
Forhindre Android fra å slå av skjermen. Mobilen vil bruke mye batteri hvis den ikke kobles til strømledning.
diff --git a/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml b/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml
index f24cdc1d85f..36e143d9171 100644
--- a/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml
+++ b/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml
@@ -2,9 +2,9 @@
UTJEVNING
Eksponentiell utjevning
- "Andre ordens algoritme for eksponentiell utjevning"
+ "Algoritme for eksponentiell utjevning, nyeste BS-verdi påvirkes"
Gjennomsnittlig utjevning
- "Gjennomsnittlig utjevnings-algoritme, nyeste verdi påvirkes ikke"
+ "Algoritme for gjennomsnittlig utjevning, nyeste BS-verdi påvirkes ikke. Kan minne om BYODA G6 sin utjevningsalgoritme"
Ingen utjevning
- "Ingen utjevning utføres på motatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6."
+ "Ingen utjevning utføres på mottatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6."
diff --git a/plugins/source/build.gradle.kts b/plugins/source/build.gradle.kts
index df0c6cf34a5..4b0ac810c4c 100644
--- a/plugins/source/build.gradle.kts
+++ b/plugins/source/build.gradle.kts
@@ -22,6 +22,8 @@ dependencies {
implementation(project(":core:ui"))
implementation(project(":core:utils"))
+ testImplementation(Libs.AndroidX.Work.testing)
+
testImplementation(project(":shared:tests"))
kapt(Libs.Dagger.compiler)
diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/PathedOTAppPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/PathedOTAppPlugin.kt
new file mode 100755
index 00000000000..b7f26e095f2
--- /dev/null
+++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/PathedOTAppPlugin.kt
@@ -0,0 +1,95 @@
+package app.aaps.plugins.source
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import androidx.work.workDataOf
+import app.aaps.core.interfaces.logging.AAPSLogger
+import app.aaps.core.interfaces.logging.LTag
+import app.aaps.core.interfaces.plugin.PluginBase
+import app.aaps.core.interfaces.plugin.PluginDescription
+import app.aaps.core.interfaces.plugin.PluginType
+import app.aaps.core.interfaces.resources.ResourceHelper
+import app.aaps.core.interfaces.source.BgSource
+import app.aaps.core.interfaces.utils.DateUtil
+import app.aaps.core.main.utils.worker.LoggingWorker
+import app.aaps.database.entities.GlucoseValue
+import app.aaps.database.impl.AppRepository
+import app.aaps.database.impl.transactions.CgmSourceTransaction
+import app.aaps.database.transactions.TransactionGlucoseValue
+import dagger.android.HasAndroidInjector
+import kotlinx.coroutines.Dispatchers
+import org.json.JSONArray
+import org.json.JSONException
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class PathedOTAppPlugin @Inject constructor(injector: HasAndroidInjector, rh: ResourceHelper, aapsLogger: AAPSLogger, )
+ : PluginBase(PluginDescription()
+ .mainType(PluginType.BGSOURCE)
+ .fragmentClass(BGSourceFragment::class.java.name)
+ .preferencesId(R.xml.pref_bgsource)
+ .pluginIcon(app.aaps.core.ui.R.mipmap.ottai_icon)
+ .pluginName(R.string.patched_ottai_app)
+ .description(R.string.description_source_patched_ottai_app),
+ aapsLogger, rh, injector), BgSource {
+
+ // cannot be inner class because of needed injection
+ class PathedOTAppWorker(context: Context, params: WorkerParameters) : LoggingWorker(context, params, Dispatchers.IO) {
+
+ @Inject lateinit var mOTAppPlugin: PathedOTAppPlugin
+ @Inject lateinit var injector: HasAndroidInjector
+ @Inject lateinit var dateUtil: DateUtil
+ @Inject lateinit var repository: AppRepository
+
+ init {
+ (context.applicationContext as HasAndroidInjector).androidInjector().inject(this)
+ }
+
+ override suspend fun doWorkAndLog(): Result {
+ var ret = Result.success()
+ if (!mOTAppPlugin.isEnabled()) return Result.success()
+ val collection = inputData.getString("collection") ?: return Result.failure(workDataOf("Error" to "missing collection"))
+ if (collection == "entries") {
+ val data = inputData.getString("data")
+ aapsLogger.debug(LTag.BGSOURCE, "Received SI App Data: $data")
+ if (!data.isNullOrEmpty()) {
+ try {
+ val glucoseValues = mutableListOf()
+ val jsonArray = JSONArray(data)
+ for (i in 0 until jsonArray.length()) {
+ val jsonObject = jsonArray.getJSONObject(i)
+ when (val type = jsonObject.getString("type")) {
+ "sgv" ->{
+ glucoseValues += TransactionGlucoseValue(
+ timestamp = jsonObject.getLong("date"),
+ value = jsonObject.getDouble("sgv"),
+ raw = jsonObject.getDouble("sgv"),
+ noise = null,
+ trendArrow = GlucoseValue.TrendArrow.fromString(jsonObject.getString("direction")),
+ sourceSensor = GlucoseValue.SourceSensor.OTApp)
+ }
+ else -> aapsLogger.debug(LTag.BGSOURCE, "Unknown entries type: $type")
+ }
+ }
+ repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), null))
+ .doOnError {
+ aapsLogger.error(LTag.DATABASE, "Error while saving values from Ottai App", it)
+ ret = Result.failure(workDataOf("Error" to it.toString()))
+ }
+ .blockingGet()
+ .also { savedValues ->
+ savedValues.all().forEach {
+ aapsLogger.debug(LTag.DATABASE, "Inserted bg $it")
+ }
+ }
+ } catch (e: JSONException) {
+ aapsLogger.error("Exception: ", e)
+ ret = Result.failure(workDataOf("Error" to e.toString()))
+ }
+ }
+ }
+ return ret
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt
index 7bcdddc2d0d..676d598f566 100644
--- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt
+++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt
@@ -1,20 +1,28 @@
package app.aaps.plugins.source
import android.content.Context
+import android.os.Bundle
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag
+import app.aaps.core.interfaces.logging.UserEntryLogger
import app.aaps.core.interfaces.plugin.PluginBase
import app.aaps.core.interfaces.plugin.PluginDescription
import app.aaps.core.interfaces.plugin.PluginType
import app.aaps.core.interfaces.receivers.Intents
import app.aaps.core.interfaces.resources.ResourceHelper
+import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.core.interfaces.source.BgSource
import app.aaps.core.interfaces.source.XDripSource
+import app.aaps.core.interfaces.utils.DateUtil
+import app.aaps.core.interfaces.utils.T
import app.aaps.core.main.utils.worker.LoggingWorker
import app.aaps.core.utils.receivers.DataWorkerStorage
import app.aaps.database.entities.GlucoseValue
+import app.aaps.database.entities.UserEntry.Action
+import app.aaps.database.entities.UserEntry.Sources
+import app.aaps.database.entities.ValueWithUnit
import app.aaps.database.impl.AppRepository
import app.aaps.database.impl.transactions.CgmSourceTransaction
import app.aaps.database.transactions.TransactionGlucoseValue
@@ -22,6 +30,7 @@ import dagger.android.HasAndroidInjector
import kotlinx.coroutines.Dispatchers
import javax.inject.Inject
import javax.inject.Singleton
+import kotlin.math.abs
import kotlin.math.round
@Singleton
@@ -34,7 +43,7 @@ class XdripSourcePlugin @Inject constructor(
.mainType(PluginType.BGSOURCE)
.fragmentClass(BGSourceFragment::class.java.name)
.pluginIcon((app.aaps.core.main.R.drawable.ic_blooddrop_48))
- .preferencesId(R.xml.pref_bgsource)
+ .preferencesId(R.xml.pref_dexcom)
.pluginName(R.string.source_xdrip)
.description(R.string.description_source_xdrip),
aapsLogger, rh, injector
@@ -52,7 +61,8 @@ class XdripSourcePlugin @Inject constructor(
GlucoseValue.SourceSensor.DEXCOM_G5_NATIVE,
GlucoseValue.SourceSensor.DEXCOM_G6_NATIVE_XDRIP,
GlucoseValue.SourceSensor.DEXCOM_G5_NATIVE_XDRIP,
- GlucoseValue.SourceSensor.DEXCOM_G6_G5_NATIVE_XDRIP
+ GlucoseValue.SourceSensor.DEXCOM_G6_G5_NATIVE_XDRIP,
+ GlucoseValue.SourceSensor.DEXCOM_G7_NATIVE_XDRIP
).any { it == glucoseValue.sourceSensor }
}
@@ -63,8 +73,25 @@ class XdripSourcePlugin @Inject constructor(
) : LoggingWorker(context, params, Dispatchers.IO) {
@Inject lateinit var xdripSourcePlugin: XdripSourcePlugin
+ @Inject lateinit var sp: SP
+ @Inject lateinit var dateUtil: DateUtil
@Inject lateinit var repository: AppRepository
@Inject lateinit var dataWorkerStorage: DataWorkerStorage
+ @Inject lateinit var uel: UserEntryLogger
+
+ fun getSensorStartTime(bundle: Bundle): Long? {
+ val now = dateUtil.now()
+ var sensorStartTime: Long? = if (sp.getBoolean(R.string.key_dexcom_log_ns_sensor_change, false)) {
+ bundle.getLong(Intents.EXTRA_SENSOR_STARTED_AT, 0)
+ } else {
+ null
+ }
+ // check start time validity
+ sensorStartTime?.let {
+ if (abs(it - now) > T.months(1).msecs() || it > now) sensorStartTime = null
+ }
+ return sensorStartTime
+ }
override suspend fun doWorkAndLog(): Result {
var ret = Result.success()
@@ -86,17 +113,27 @@ class XdripSourcePlugin @Inject constructor(
?: ""
)
)
- repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), null))
+ val sensorStartTime = getSensorStartTime(bundle)
+ repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), sensorStartTime))
.doOnError {
aapsLogger.error(LTag.DATABASE, "Error while saving values from Xdrip", it)
ret = Result.failure(workDataOf("Error" to it.toString()))
}
.blockingGet()
- .also { savedValues ->
- savedValues.all().forEach {
+ .also { result ->
+ result.all().forEach {
xdripSourcePlugin.detectSource(it)
aapsLogger.debug(LTag.DATABASE, "Inserted bg $it")
}
+ result.sensorInsertionsInserted.forEach {
+ uel.log(
+ Action.CAREPORTAL,
+ Sources.Xdrip,
+ ValueWithUnit.Timestamp(it.timestamp),
+ ValueWithUnit.TherapyEventType(it.type)
+ )
+ aapsLogger.debug(LTag.DATABASE, "Inserted sensor insertion $it")
+ }
}
xdripSourcePlugin.sensorBatteryLevel = bundle.getInt(Intents.EXTRA_SENSOR_BATTERY, -1)
return ret
diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/di/SourceModule.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/di/SourceModule.kt
index de7a9433b1c..35b13cf797b 100644
--- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/di/SourceModule.kt
+++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/di/SourceModule.kt
@@ -10,6 +10,7 @@ import app.aaps.plugins.source.EversensePlugin
import app.aaps.plugins.source.GlimpPlugin
import app.aaps.plugins.source.MM640gPlugin
import app.aaps.plugins.source.NSClientSourcePlugin
+import app.aaps.plugins.source.PathedOTAppPlugin
import app.aaps.plugins.source.PathedSIAppPlugin
import app.aaps.plugins.source.PathedSinoAppPlugin
import app.aaps.plugins.source.PoctechPlugin
@@ -34,6 +35,7 @@ abstract class SourceModule {
@ContributesAndroidInjector abstract fun contributesXdripWorker(): XdripSourcePlugin.XdripSourceWorker
@ContributesAndroidInjector abstract fun contributesDexcomWorker(): DexcomPlugin.DexcomWorker
@ContributesAndroidInjector abstract fun contributesMM640gWorker(): MM640gPlugin.MM640gWorker
+ @ContributesAndroidInjector abstract fun contributesOTAppWorker(): PathedOTAppPlugin.PathedOTAppWorker
@ContributesAndroidInjector abstract fun contributesSIAppWorker(): PathedSIAppPlugin.PathedSIAppWorker
@ContributesAndroidInjector abstract fun contributesSinoAppWorker(): PathedSinoAppPlugin.PathedSinoAppWorker
@ContributesAndroidInjector abstract fun contributesGlimpWorker(): GlimpPlugin.GlimpWorker
diff --git a/plugins/source/src/main/res/values-zh-rCN/strings.xml b/plugins/source/src/main/res/values-zh-rCN/strings.xml
index 209f02a1111..85dd88f7db5 100644
--- a/plugins/source/src/main/res/values-zh-rCN/strings.xml
+++ b/plugins/source/src/main/res/values-zh-rCN/strings.xml
@@ -20,6 +20,9 @@
Aidex动泰
从GlucoRx Aidex动泰持续葡萄糖监测系统接收血糖值。
血糖上传设置
+
+ 欧态-小欧生态
+ 从欧态易测APP接收血糖值。
X基补丁版
从X基补丁版App接收血糖值。
diff --git a/plugins/source/src/main/res/values/strings.xml b/plugins/source/src/main/res/values/strings.xml
index 45c6708bbb6..db885a76767 100644
--- a/plugins/source/src/main/res/values/strings.xml
+++ b/plugins/source/src/main/res/values/strings.xml
@@ -43,6 +43,9 @@
Create event \"Sensor Change\" in NS automatically on sensor start
direction
+
+ 欧态-小欧生态
+ 从欧态易测APP接收血糖值。
Patched SI App
Patched Sino App
diff --git a/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt b/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt
index 3e645fb3e9f..6afe6d546a2 100644
--- a/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt
+++ b/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt
@@ -1,25 +1,115 @@
package app.aaps.plugins.source
+import android.content.Context
+import android.os.Bundle
+import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
+import androidx.work.testing.TestListenableWorkerBuilder
+import app.aaps.core.interfaces.receivers.Intents
import app.aaps.core.interfaces.resources.ResourceHelper
+import app.aaps.core.interfaces.utils.DateUtil
+import app.aaps.core.interfaces.utils.T
+import app.aaps.core.interfaces.sharedPreferences.SP
+import app.aaps.core.utils.receivers.DataWorkerStorage
+import app.aaps.shared.impl.utils.DateUtilImpl
+import app.aaps.shared.tests.BundleMock
import app.aaps.shared.tests.TestBase
import com.google.common.truth.Truth.assertThat
import dagger.android.AndroidInjector
+import dagger.android.HasAndroidInjector
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mock
+import org.mockito.Mockito.`when`
class XdripSourcePluginTest : TestBase() {
+ abstract class ContextWithInjector : Context(), HasAndroidInjector
+
private lateinit var xdripSourcePlugin: XdripSourcePlugin
+ private lateinit var dateUtil: DateUtil
+ private lateinit var dataWorkerStorage: DataWorkerStorage
+
+ private val injector = HasAndroidInjector {
+ AndroidInjector {
+ if (it is XdripSourcePlugin.XdripSourceWorker) {
+ it.dataWorkerStorage = dataWorkerStorage
+ it.dateUtil = dateUtil
+ it.sp = sp
+ }
+ }
+ }
+ @Mock lateinit var sp: SP
@Mock lateinit var rh: ResourceHelper
+ @Mock lateinit var context: ContextWithInjector
@BeforeEach
fun setup() {
- xdripSourcePlugin = XdripSourcePlugin({ AndroidInjector { } }, rh, aapsLogger)
+ `when`(context.applicationContext).thenReturn(context)
+ `when`(context.androidInjector()).thenReturn(injector.androidInjector())
+ xdripSourcePlugin = XdripSourcePlugin(injector, rh, aapsLogger)
+ dateUtil = DateUtilImpl(context)
+ dataWorkerStorage = DataWorkerStorage(context)
+ }
+
+ private fun prepareWorker(
+ sensorStartTime: Long? = dateUtil.now(),
+ logNsSensorChange: Boolean = true,
+ ): Pair {
+ val bundle = BundleMock.mock()
+ sensorStartTime?.let { bundle.putLong(Intents.EXTRA_SENSOR_STARTED_AT, sensorStartTime) }
+ `when`(sp.getBoolean(R.string.key_dexcom_log_ns_sensor_change, false)).thenReturn(logNsSensorChange)
+
+ lateinit var worker: XdripSourcePlugin.XdripSourceWorker
+ TestListenableWorkerBuilder(context)
+ .setWorkerFactory(object: WorkerFactory() {
+ override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): XdripSourcePlugin.XdripSourceWorker {
+ worker = XdripSourcePlugin.XdripSourceWorker(context, workerParameters)
+ return worker
+ }
+ })
+ .setInputData(dataWorkerStorage.storeInputData(bundle, Intents.ACTION_NEW_BG_ESTIMATE)).build()
+
+ return Pair(bundle, worker)
}
@Test fun advancedFilteringSupported() {
assertThat(xdripSourcePlugin.advancedFilteringSupported()).isFalse()
}
+
+ @Test fun getSensorStartTime_withoutValue_returnsNull() {
+ val (bundle, worker) = prepareWorker(sensorStartTime = null)
+
+ val result = worker.getSensorStartTime(bundle)
+
+ assertThat(result).isNull()
+ }
+
+ @Test fun getSensorStartTime_withSettingDisabled_returnsNull() {
+ val sensorStartTime = dateUtil.now()
+ val (bundle, worker) = prepareWorker(sensorStartTime = sensorStartTime, logNsSensorChange = false)
+
+ val result = worker.getSensorStartTime(bundle)
+
+ assertThat(result).isNull()
+ }
+
+ @Test fun getSensorStartTime_withRecentValue_returnsStartTime() {
+ val sensorStartTime = dateUtil.now()
+ val (bundle, worker) = prepareWorker(sensorStartTime = sensorStartTime)
+
+ val result = worker.getSensorStartTime(bundle)
+
+ assertThat(result).isEqualTo(sensorStartTime)
+ }
+
+ @Test fun getSensorStartTime_withOldValue_returnsNull() {
+ val sensorStartTime = dateUtil.now() - T.months(2).msecs()
+ val (bundle, worker) = prepareWorker(sensorStartTime = sensorStartTime)
+
+ val result = worker.getSensorStartTime(bundle)
+
+ assertThat(result).isNull()
+ }
}
diff --git a/plugins/sync/build.gradle.kts b/plugins/sync/build.gradle.kts
index b6b885370c3..3261c6db025 100644
--- a/plugins/sync/build.gradle.kts
+++ b/plugins/sync/build.gradle.kts
@@ -30,6 +30,7 @@ dependencies {
testImplementation(project(":shared:tests"))
testImplementation(project(":implementation"))
testImplementation(project(":plugins:aps"))
+ androidTestImplementation(project(":shared:tests"))
// OpenHuman
api(Libs.Squareup.Okhttp3.okhttp)
@@ -49,6 +50,10 @@ dependencies {
// DataLayerListenerService
api(Libs.Google.Android.PlayServices.wearable)
+ // Garmin
+ api(Libs.connectiqSdk)
+ androidTestImplementation(Libs.connectiqSdk)
+
kapt(Libs.Dagger.compiler)
kapt(Libs.Dagger.androidProcessor)
}
\ No newline at end of file
diff --git a/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt b/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt
new file mode 100644
index 00000000000..4f63ea3671a
--- /dev/null
+++ b/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt
@@ -0,0 +1,301 @@
+package app.aaps.plugins.sync.garmin
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.ServiceConnection
+import android.os.Binder
+import android.os.IBinder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import app.aaps.shared.tests.TestBase
+import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService
+import com.garmin.android.connectiq.ConnectIQ
+import com.garmin.android.connectiq.IQApp
+import com.garmin.android.connectiq.IQDevice
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+class GarminDeviceClientTest: TestBase() {
+ private val serviceDescriptor = "com.garmin.android.apps.connectmobile.connectiq.IConnectIQService"
+ private lateinit var client: GarminDeviceClient
+ private lateinit var serviceConnection: ServiceConnection
+ private lateinit var device: GarminDevice
+ private val packageName = "TestPackage"
+ private val actions = mutableMapOf()
+ // Maps app ids to intent actions.
+ private val receivers = mutableMapOf()
+
+ private val receiver = mock()
+ private val binder = mock() {
+ on { isBinderAlive } doReturn true
+ }
+ private val ciqService = mock() {
+ on { asBinder() } doReturn binder
+ on { connectedDevices } doReturn listOf(IQDevice(1L, "TDevice"))
+ on { registerApp(any(), any(), any()) }.doAnswer { i ->
+ receivers[i.getArgument(0).applicationId] = i.getArgument(1)
+ }
+ }
+ private val context = mock() {
+ on { packageName } doReturn this@GarminDeviceClientTest.packageName
+ on { registerReceiver(any(), any()) } doAnswer { i ->
+ actions[i.getArgument(1).getAction(0)] = i.getArgument(0)
+ Intent()
+ }
+ on { unregisterReceiver(any()) } doAnswer { i ->
+ val keys = actions.entries.filter {(_, br) -> br == i.getArgument(0) }.map { (k, _) -> k }
+ keys.forEach { k -> actions.remove(k) }
+ }
+ on { bindService(any(), eq(Context.BIND_AUTO_CREATE), any(), any()) }. doAnswer { i ->
+ serviceConnection = i.getArgument(3)
+ i.getArgument(2).execute {
+ serviceConnection.onServiceConnected(
+ GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT,
+ Binder().apply { attachInterface(ciqService, serviceDescriptor) })
+ }
+ true
+ }
+ on { bindService(any(), any(), eq(Context.BIND_AUTO_CREATE)) }. doAnswer { i ->
+ serviceConnection = i.getArgument(1)
+ serviceConnection.onServiceConnected(
+ GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT,
+ Binder().apply { attachInterface(ciqService, serviceDescriptor) })
+ true
+ }
+ }
+
+ @Before
+ fun setup() {
+ client = GarminDeviceClient(aapsLogger, context, receiver, retryWaitFactor = 0L)
+ device = GarminDevice(client, 1L, "TDevice")
+ verify(receiver, timeout(2_000L)).onConnect(client)
+ }
+
+ @After
+ fun shutdown() {
+ if (::client.isInitialized) client.dispose()
+ assertEquals(0, actions.size) // make sure all broadcastReceivers were unregistered
+ verify(context).unbindService(serviceConnection)
+ }
+
+ @Test
+ fun connect() {
+ }
+
+ @Test
+ fun disconnect() {
+ serviceConnection.onServiceDisconnected(GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT)
+ verify(receiver).onDisconnect(client)
+ assertEquals(0, actions.size)
+ }
+
+ @Test
+ fun connectedDevices() {
+ assertEquals(listOf(device), client.connectedDevices)
+ verify(ciqService).connectedDevices
+ }
+
+ @Test
+ fun reconnectDeadBinder() {
+ whenever(binder.isBinderAlive).thenReturn(false, true)
+ assertEquals(listOf(device), client.connectedDevices)
+
+ verify(ciqService).connectedDevices
+ verify(ciqService, times(2)).asBinder()
+ verify(context, times(2))
+ .bindService(any(), eq(Context.BIND_AUTO_CREATE), any(), any())
+
+ verifyNoMoreInteractions(ciqService)
+ verifyNoMoreInteractions(receiver)
+ }
+
+ @Test
+ fun sendMessage() {
+ val appId = "APPID1"
+ val data = "Hello, World!".toByteArray()
+
+ client.sendMessage(GarminApplication(device, appId, "$appId-name"), data)
+ verify(ciqService).sendMessage(
+ argThat { iqMsg -> data.contentEquals(iqMsg.messageData)
+ && iqMsg.notificationPackage == packageName
+ && iqMsg.notificationAction == client.sendMessageAction },
+ argThat { iqDevice -> iqDevice.deviceIdentifier == device.id },
+ argThat { iqApp -> iqApp?.applicationId == appId })
+
+ val intent = Intent().apply {
+ putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal)
+ putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice())
+ putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId)
+ }
+ actions[client.sendMessageAction]!!.onReceive(context, intent)
+ actions[client.sendMessageAction]!!.onReceive(context, intent) // extra on receive will be ignored
+ verify(receiver).onSendMessage(client, device.id, appId, null)
+ }
+
+ @Test
+ fun sendMessage_failNoRetry() {
+ val appId = "APPID1"
+ val data = "Hello, World!".toByteArray()
+
+ client.sendMessage(GarminApplication(device, appId, "$appId-name"), data)
+ verify(ciqService).sendMessage(
+ argThat { iqMsg -> data.contentEquals(iqMsg.messageData)
+ && iqMsg.notificationPackage == packageName
+ && iqMsg.notificationAction == client.sendMessageAction },
+ argThat {iqDevice -> iqDevice.deviceIdentifier == device.id },
+ argThat { iqApp -> iqApp?.applicationId == appId })
+
+ val intent = Intent().apply {
+ putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.FAILURE_MESSAGE_TOO_LARGE.ordinal)
+ putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice())
+ putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId)
+ }
+ actions[client.sendMessageAction]!!.onReceive(context, intent)
+ verify(receiver).onSendMessage(client, device.id, appId, "error FAILURE_MESSAGE_TOO_LARGE")
+ }
+
+ @Test
+ fun sendMessage_failRetry() {
+ val appId = "APPID1"
+ val data = "Hello, World!".toByteArray()
+
+ client.sendMessage(GarminApplication(device, appId, "$appId-name"), data)
+ verify(ciqService).sendMessage(
+ argThat { iqMsg -> data.contentEquals(iqMsg.messageData)
+ && iqMsg.notificationPackage == packageName
+ && iqMsg.notificationAction == client.sendMessageAction },
+ argThat {iqDevice -> iqDevice.deviceIdentifier == device.id },
+ argThat { iqApp -> iqApp?.applicationId == appId })
+
+ val intent = Intent().apply {
+ putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.FAILURE_DURING_TRANSFER.ordinal)
+ putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice())
+ putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId)
+ }
+ actions[client.sendMessageAction]!!.onReceive(context, intent)
+ verifyNoMoreInteractions(receiver)
+
+ // Verify retry ...
+ verify(ciqService, timeout(10_000L).times( 2)).sendMessage(
+ argThat { iqMsg -> data.contentEquals(iqMsg.messageData)
+ && iqMsg.notificationPackage == packageName
+ && iqMsg.notificationAction == client.sendMessageAction },
+ argThat {iqDevice -> iqDevice.deviceIdentifier == device.id },
+ argThat { iqApp -> iqApp?.applicationId == appId })
+
+ intent.putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal)
+ actions[client.sendMessageAction]!!.onReceive(context, intent)
+ verify(receiver).onSendMessage(client, device.id, appId, null)
+ }
+
+ @Test
+ fun sendMessage_2toSameApp() {
+ val appId = "APPID1"
+ val data1 = "m1".toByteArray()
+ val data2 = "m2".toByteArray()
+
+ client.sendMessage(GarminApplication(device, appId, "$appId-name"), data1)
+ client.sendMessage(GarminApplication(device, appId, "$appId-name"), data2)
+ verify(ciqService).sendMessage(
+ argThat { iqMsg -> data1.contentEquals(iqMsg.messageData)
+ && iqMsg.notificationPackage == packageName
+ && iqMsg.notificationAction == client.sendMessageAction },
+ argThat {iqDevice -> iqDevice.deviceIdentifier == device.id },
+ argThat { iqApp -> iqApp?.applicationId == appId })
+ verify(ciqService, atLeastOnce()).asBinder()
+ verifyNoMoreInteractions(ciqService)
+
+ val intent = Intent().apply {
+ putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal)
+ putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice())
+ putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId)
+ }
+ actions[client.sendMessageAction]!!.onReceive(context, intent)
+ verify(receiver).onSendMessage(client, device.id, appId, null)
+
+ verify(ciqService, timeout(5000L)).sendMessage(
+ argThat { iqMsg -> data2.contentEquals(iqMsg.messageData)
+ && iqMsg.notificationPackage == packageName
+ && iqMsg.notificationAction == client.sendMessageAction },
+ argThat {iqDevice -> iqDevice.deviceIdentifier == device.id },
+ argThat { iqApp -> iqApp?.applicationId == appId })
+
+ actions[client.sendMessageAction]!!.onReceive(context, intent)
+ verify(receiver, times(2)).onSendMessage(client, device.id, appId, null)
+ }
+
+ @Test
+ fun sendMessage_2to2Apps() {
+ val appId1 = "APPID1"
+ val appId2 = "APPID2"
+ val data1 = "m1".toByteArray()
+ val data2 = "m2".toByteArray()
+
+ client.sendMessage(GarminApplication(device, appId1, "$appId1-name"), data1)
+ client.sendMessage(GarminApplication(device, appId2, "$appId2-name"), data2)
+ verify(ciqService).sendMessage(
+ argThat { iqMsg -> data1.contentEquals(iqMsg.messageData)
+ && iqMsg.notificationPackage == packageName
+ && iqMsg.notificationAction == client.sendMessageAction },
+ argThat {iqDevice -> iqDevice.deviceIdentifier == device.id },
+ argThat { iqApp -> iqApp?.applicationId == appId1 })
+ verify(ciqService, timeout(5000L)).sendMessage(
+ argThat { iqMsg -> data2.contentEquals(iqMsg.messageData)
+ && iqMsg.notificationPackage == packageName
+ && iqMsg.notificationAction == client.sendMessageAction },
+ argThat {iqDevice -> iqDevice.deviceIdentifier == device.id },
+ argThat { iqApp -> iqApp?.applicationId == appId2 })
+
+ val intent1 = Intent().apply {
+ putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal)
+ putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice())
+ putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId1)
+ }
+ actions[client.sendMessageAction]!!.onReceive(context, intent1)
+ verify(receiver).onSendMessage(client, device.id, appId1, null)
+
+ val intent2 = Intent().apply {
+ putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal)
+ putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice())
+ putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId2)
+ }
+ actions[client.sendMessageAction]!!.onReceive(context, intent2)
+ verify(receiver).onSendMessage(client, device.id, appId2, null)
+ }
+
+ @Test
+ fun receiveMessage() {
+ val app = GarminApplication(GarminDevice(client, 1L, "D1"), "APPID1", "N1")
+ client.registerForMessages(app)
+ assertTrue(receivers.contains(app.id))
+ val intent = Intent().apply {
+ putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, app.device.toIQDevice())
+ putExtra(GarminDeviceClient.EXTRA_PAYLOAD, "foo".toByteArray())
+ }
+ actions[receivers[app.id]]!!.onReceive(context, intent)
+ verify(receiver).onReceiveMessage(
+ eq(client),
+ eq(app.device.id),
+ eq(app.id),
+ argThat { payload -> "foo" == String(payload) })
+ }
+}
\ No newline at end of file
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt
new file mode 100644
index 00000000000..6ff37f0a627
--- /dev/null
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt
@@ -0,0 +1,32 @@
+package app.aaps.plugins.sync.garmin
+
+data class GarminApplication(
+ val device: GarminDevice,
+ val id: String,
+ val name: String?) {
+
+ val client get() = device.client
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GarminApplication
+
+ if (client != other.client) return false
+ if (device != other.device) return false
+ if (id != other.id) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = client.hashCode()
+ result = 31 * result + device.hashCode()
+ result = 31 * result + id.hashCode()
+ return result
+ }
+
+ override fun toString() = "A[$device:$id:$name]"
+}
+
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt
new file mode 100644
index 00000000000..6333f2ae825
--- /dev/null
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt
@@ -0,0 +1,16 @@
+package app.aaps.plugins.sync.garmin
+
+import io.reactivex.rxjava3.disposables.Disposable
+
+interface GarminClient: Disposable {
+ /** Name of the client. */
+ val name: String
+
+ val connectedDevices: List
+
+ /** Register to receive messages from the given up. */
+ fun registerForMessages(app: GarminApplication)
+
+ /** Asynchronously sends a message to an application. */
+ fun sendMessage(app: GarminApplication, data: ByteArray)
+}
\ No newline at end of file
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt
new file mode 100644
index 00000000000..1df255d38a5
--- /dev/null
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt
@@ -0,0 +1,36 @@
+package app.aaps.plugins.sync.garmin
+
+import com.garmin.android.connectiq.IQDevice
+
+data class GarminDevice(
+ val client: GarminClient,
+ val id: Long,
+ var name: String) {
+
+ constructor(client: GarminClient, iqDevice: IQDevice): this(
+ client,
+ iqDevice.deviceIdentifier,
+ iqDevice.friendlyName) {}
+
+ override fun toString(): String = "D[$name/$id]"
+
+ fun toIQDevice() = IQDevice(id, name)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GarminDevice
+
+ if (client != other.client) return false
+ if (id != other.id) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = client.hashCode()
+ result = 31 * result + id.hashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt
new file mode 100644
index 00000000000..ec81ddb5125
--- /dev/null
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt
@@ -0,0 +1,292 @@
+package app.aaps.plugins.sync.garmin
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.IBinder
+import app.aaps.core.interfaces.logging.AAPSLogger
+import app.aaps.core.interfaces.logging.LTag
+import app.aaps.core.utils.waitMillis
+import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService
+import com.garmin.android.connectiq.ConnectIQ.IQMessageStatus
+import com.garmin.android.connectiq.IQApp
+import com.garmin.android.connectiq.IQDevice
+import com.garmin.android.connectiq.IQMessage
+import io.reactivex.rxjava3.disposables.Disposable
+import io.reactivex.rxjava3.schedulers.Schedulers
+import org.jetbrains.annotations.VisibleForTesting
+import java.lang.Thread.UncaughtExceptionHandler
+import java.time.Instant
+import java.util.LinkedList
+import java.util.Queue
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+
+/** GarminClient that talks via the ConnectIQ app to a physical device. */
+class GarminDeviceClient(
+ private val aapsLogger: AAPSLogger,
+ private val context: Context,
+ private val receiver: GarminReceiver,
+ private val retryWaitFactor: Long = 5L): Disposable, GarminClient {
+
+ override val name = "Device"
+ private var executor = Executors.newSingleThreadExecutor { r ->
+ Thread(r).apply {
+ name = "Garmin callback"
+ isDaemon = true
+ uncaughtExceptionHandler = UncaughtExceptionHandler { _, e ->
+ aapsLogger.error(LTag.GARMIN, "ConnectIQ callback failed", e) }
+ }
+ }
+ private var bindLock = Object()
+ private var ciqService: IConnectIQService? = null
+ get() {
+ synchronized (bindLock) {
+ if (field?.asBinder()?.isBinderAlive != true) {
+ field = null
+ if (state !in arrayOf(State.BINDING, State.RECONNECTING)) {
+ aapsLogger.info(LTag.GARMIN, "reconnecting to ConnectIQ service")
+ state = State.RECONNECTING
+ bindService()
+ }
+ bindLock.waitMillis(2_000L)
+ if (field?.asBinder()?.isBinderAlive != true) {
+ field = null
+ // The [serviceConnection] didn't have a chance to reassign ciqService,
+ // i.e. the wait timed out. Give up.
+ aapsLogger.warn(LTag.GARMIN, "no ciqservice $this")
+ }
+ }
+ return field
+ }
+ }
+
+ private val registeredActions = mutableSetOf()
+ private val broadcastReceiver = mutableListOf()
+ private var state = State.DISCONNECTED
+ private val serviceIntent get() = Intent(CONNECTIQ_SERVICE_ACTION).apply {
+ component = CONNECTIQ_SERVICE_COMPONENT }
+
+ @VisibleForTesting
+ val sendMessageAction = createAction("SEND_MESSAGE")
+
+ private enum class State {
+ BINDING,
+ CONNECTED,
+ DISCONNECTED,
+ DISPOSED,
+ RECONNECTING,
+ }
+
+ private val ciqServiceConnection = object: ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ var notifyReceiver: Boolean
+ val ciq: IConnectIQService
+ synchronized(bindLock) {
+ aapsLogger.info(LTag.GARMIN, "ConnectIQ App connected")
+ ciq = IConnectIQService.Stub.asInterface(service)
+ notifyReceiver = state != State.RECONNECTING
+ state = State.CONNECTED
+ ciqService = ciq
+ bindLock.notifyAll()
+ }
+ if (notifyReceiver) receiver.onConnect(this@GarminDeviceClient)
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ synchronized(bindLock) {
+ aapsLogger.info(LTag.GARMIN, "ConnectIQ App disconnected")
+ ciqService = null
+ if (state != State.DISPOSED) state = State.DISCONNECTED
+ }
+ broadcastReceiver.forEach { br -> context.unregisterReceiver(br) }
+ broadcastReceiver.clear()
+ registeredActions.clear()
+ receiver.onDisconnect(this@GarminDeviceClient)
+ }
+ }
+
+ init {
+ aapsLogger.info(LTag.GARMIN, "binding to ConnectIQ service")
+ registerReceiver(sendMessageAction, ::onSendMessage)
+ state = State.BINDING
+ bindService()
+ }
+
+ private fun bindService() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ context.bindService(serviceIntent, Context.BIND_AUTO_CREATE, executor, ciqServiceConnection)
+ } else {
+ context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE)
+ }
+ }
+
+ override val connectedDevices: List
+ get() = ciqService?.connectedDevices?.map { iqDevice -> GarminDevice(this, iqDevice) }
+ ?: emptyList()
+
+ override fun isDisposed() = state == State.DISPOSED
+ override fun dispose() {
+ broadcastReceiver.forEach { context.unregisterReceiver(it) }
+ broadcastReceiver.clear()
+ registeredActions.clear()
+ try {
+ context.unbindService(ciqServiceConnection)
+ } catch (e: Exception) {
+ aapsLogger.warn(LTag.GARMIN, "unbind CIQ failed ${e.message}")
+ }
+ state = State.DISPOSED
+ }
+
+ /** Creates a unique action name for ConnectIQ callbacks. */
+ private fun createAction(action: String) = "${javaClass.`package`!!.name}.$action"
+
+ /** Registers a callback [BroadcastReceiver] under the given action that will
+ * used by the ConnectIQ app for callbacks.*/
+ private fun registerReceiver(action: String, receive: (intent: Intent) -> Unit) {
+ val recv = object: BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) { receive(intent) }
+ }
+ broadcastReceiver.add(recv)
+ context.registerReceiver(recv, IntentFilter(action))
+ }
+
+ override fun registerForMessages(app: GarminApplication) {
+ aapsLogger.info(LTag.GARMIN, "registerForMessage $name $app")
+ val action = createAction("ON_MESSAGE_${app.device.id}_${app.id}")
+ val iqApp = IQApp(app.id)
+ synchronized (registeredActions) {
+ if (!registeredActions.contains(action)) {
+ registerReceiver(action) { intent: Intent -> onReceiveMessage(iqApp, intent) }
+ ciqService?.registerApp(iqApp, action, context.packageName)
+ registeredActions.add(action)
+ } else {
+ aapsLogger.info(LTag.GARMIN, "registerForMessage $action already registered")
+ }
+ }
+ }
+
+ @Suppress("Deprecation")
+ private fun onReceiveMessage(iqApp: IQApp, intent: Intent) {
+ val iqDevice = intent.getParcelableExtra(EXTRA_REMOTE_DEVICE) as IQDevice?
+ val data = intent.getByteArrayExtra(EXTRA_PAYLOAD)
+ if (iqDevice != null && data != null)
+ receiver.onReceiveMessage(this, iqDevice.deviceIdentifier, iqApp.applicationId, data)
+ }
+
+ /** Receives callback from ConnectIQ about message transfers. */
+ private fun onSendMessage(intent: Intent) {
+ val statusOrd = intent.getIntExtra(EXTRA_STATUS, IQMessageStatus.FAILURE_UNKNOWN.ordinal)
+ val status = IQMessageStatus.values().firstOrNull { s -> s.ordinal == statusOrd } ?: IQMessageStatus.FAILURE_UNKNOWN
+ val deviceId = getDevice(intent)
+ val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.uppercase()
+ if (deviceId == null || appId == null) {
+ aapsLogger.warn(LTag.GARMIN, "onSendMessage device='$deviceId' app='$appId'")
+ } else {
+ synchronized (messageQueues) {
+ val queue = messageQueues[deviceId to appId]
+ val msg = queue?.peek()
+ if (queue == null || msg == null) {
+ aapsLogger.warn(LTag.GARMIN, "onSendMessage unknown message $deviceId, $appId, $status")
+ return
+ }
+
+ var errorMessage: String? = null
+ when (status) {
+ IQMessageStatus.SUCCESS -> {}
+ IQMessageStatus.FAILURE_DEVICE_NOT_CONNECTED,
+ IQMessageStatus.FAILURE_DURING_TRANSFER -> {
+ if (msg.attempt < MAX_RETRIES) {
+ val delaySec = retryWaitFactor * msg.attempt
+ Schedulers.io().scheduleDirect({ retryMessage(deviceId, appId) }, delaySec, TimeUnit.SECONDS)
+ return
+ }
+ }
+ else -> {
+ errorMessage = "error $status"
+ }
+ }
+ queue.poll()
+ receiver.onSendMessage(this, msg.app.device.id, msg.app.id, errorMessage)
+ if (queue.isNotEmpty()) {
+ Schedulers.io().scheduleDirect { retryMessage(deviceId, appId) }
+ }
+ }
+ }
+ }
+
+ @Suppress("Deprecation")
+ private fun getDevice(intent: Intent): Long? {
+ val rawDevice = intent.extras?.get(EXTRA_REMOTE_DEVICE)
+ return if (rawDevice is Long) rawDevice else (rawDevice as IQDevice?)?.deviceIdentifier
+ ?: return null
+ }
+
+ private class Message(
+ val app: GarminApplication,
+ val data: ByteArray) {
+ var attempt: Int = 0
+ val creation = Instant.now()
+ var lastAttempt: Instant? = null
+ val iqApp get() = IQApp(app.id, app.name, 0)
+ val iqDevice get() = app.device.toIQDevice()
+ }
+
+ private val messageQueues = mutableMapOf, Queue> ()
+
+ override fun sendMessage(app: GarminApplication, data: ByteArray) {
+ val msg = synchronized (messageQueues) {
+ val msg = Message(app, data)
+ val oldMessageCutOff = Instant.now().minusSeconds(30)
+ val queue = messageQueues.getOrPut(app.device.id to app.id) { LinkedList() }
+ while (true) {
+ val oldMsg = queue.peek() ?: break
+ if ((oldMsg.lastAttempt ?: oldMsg.creation).isBefore(oldMessageCutOff)) {
+ aapsLogger.warn(LTag.GARMIN, "remove old msg ${msg.app}")
+ queue.poll()
+ } else {
+ break
+ }
+ }
+ queue.add(msg)
+ // Make sure we have only one outstanding message per app, so we ensure
+ // that always the first message in the queue is currently send.
+ if (queue.size == 1) msg else null
+ }
+ if (msg != null) sendMessage(msg)
+ }
+
+ private fun retryMessage(deviceId: Long, appId: String) {
+ val msg = synchronized (messageQueues) {
+ messageQueues[deviceId to appId]?.peek() ?: return
+ }
+ sendMessage(msg)
+ }
+
+ private fun sendMessage(msg: Message) {
+ msg.attempt++
+ msg.lastAttempt = Instant.now()
+ val iqMsg = IQMessage(msg.data, context.packageName, sendMessageAction)
+ ciqService?.sendMessage(iqMsg, msg.iqDevice, msg.iqApp)
+ }
+
+ override fun toString() = "$name[$state]"
+
+ companion object {
+ const val CONNECTIQ_SERVICE_ACTION = "com.garmin.android.apps.connectmobile.CONNECTIQ_SERVICE_ACTION"
+ const val EXTRA_APPLICATION_ID = "com.garmin.android.connectiq.EXTRA_APPLICATION_ID"
+ const val EXTRA_REMOTE_DEVICE = "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE"
+ const val EXTRA_PAYLOAD = "com.garmin.android.connectiq.EXTRA_PAYLOAD"
+ const val EXTRA_STATUS = "com.garmin.android.connectiq.EXTRA_STATUS"
+ val CONNECTIQ_SERVICE_COMPONENT = ComponentName(
+ "com.garmin.android.apps.connectmobile",
+ "com.garmin.android.apps.connectmobile.connectiq.ConnectIQService")
+
+ const val MAX_RETRIES = 10
+ }
+}
\ No newline at end of file
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt
new file mode 100644
index 00000000000..feaf1743fc9
--- /dev/null
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt
@@ -0,0 +1,123 @@
+package app.aaps.plugins.sync.garmin
+
+import android.content.Context
+import app.aaps.core.interfaces.logging.AAPSLogger
+import app.aaps.core.interfaces.logging.LTag
+import io.reactivex.rxjava3.disposables.Disposable
+
+class GarminMessenger(
+ private val aapsLogger: AAPSLogger,
+ private val context: Context,
+ applicationIdNames: Map,
+ private val messageCallback: (app: GarminApplication, msg: Any) -> Unit,
+ enableConnectIq: Boolean,
+ enableSimulator: Boolean): Disposable, GarminReceiver {
+
+ private var disposed: Boolean = false
+ /** All devices that where connected since this instance was created. */
+ private val devices = mutableMapOf()
+ private val clients = mutableListOf()
+ private val appIdNames = mutableMapOf()
+ init {
+ aapsLogger.info(LTag.GARMIN, "init CIQ debug=$enableSimulator")
+ appIdNames.putAll(applicationIdNames)
+ if (enableConnectIq) startDeviceClient()
+ if (enableSimulator) {
+ appIdNames["SimApp"] = "SimulatorApp"
+ GarminSimulatorClient(aapsLogger, this)
+ }
+ }
+
+ private fun getDevice(client: GarminClient, deviceId: Long): GarminDevice {
+ synchronized (devices) {
+ return devices.getOrPut(deviceId) {
+ client.connectedDevices.firstOrNull { d -> d.id == deviceId } ?:
+ GarminDevice(client, deviceId, "unknown") }
+ }
+ }
+
+ private fun getApplication(client: GarminClient, deviceId: Long, appId: String): GarminApplication {
+ return GarminApplication(getDevice(client, deviceId), appId, appIdNames[appId])
+ }
+
+ private fun startDeviceClient() {
+ GarminDeviceClient(aapsLogger, context, this)
+ }
+
+ override fun onConnect(client: GarminClient) {
+ aapsLogger.info(LTag.GARMIN, "onConnect $client")
+ clients.add(client)
+ }
+
+ override fun onDisconnect(client: GarminClient) {
+ aapsLogger.info(LTag.GARMIN, "onDisconnect ${client.name}")
+ clients.remove(client)
+ synchronized (devices) {
+ val deviceIds = devices.filter { (_, d) -> d.client == client }.map { (id, _) -> id }
+ deviceIds.forEach { id -> devices.remove(id) }
+ }
+ client.dispose()
+ when (client) {
+ is GarminDeviceClient -> startDeviceClient()
+ is GarminSimulatorClient -> GarminSimulatorClient(aapsLogger, this)
+ else -> aapsLogger.warn(LTag.GARMIN, "onDisconnect unknown client $client")
+ }
+ }
+
+ override fun onReceiveMessage(client: GarminClient, deviceId: Long, appId: String, data: ByteArray) {
+ val app = getApplication(client, deviceId, appId)
+ val msg = GarminSerializer.deserialize(data)
+ if (msg == null) {
+ aapsLogger.warn(LTag.GARMIN, "receive NULL msg")
+ } else {
+ aapsLogger.info(LTag.GARMIN, "receive ${data.size} bytes")
+ messageCallback(app, msg)
+ }
+ }
+
+ /** Receives status notifications for a sent message. */
+ override fun onSendMessage(client: GarminClient, deviceId: Long, appId: String, errorMessage: String?) {
+ val app = getApplication(client, deviceId, appId)
+ aapsLogger.info(LTag.GARMIN, "onSendMessage $app ${errorMessage ?: "OK"}")
+ }
+
+ fun sendMessage(device: GarminDevice, msg: Any) {
+ appIdNames.forEach { (appId, _) ->
+ sendMessage(getApplication(device.client, device.id, appId), msg)
+ }
+ }
+
+ /** Sends a message to all applications on all devices. */
+ fun sendMessage(msg: Any) {
+ clients.forEach { cl -> cl.connectedDevices.forEach { d -> sendMessage(d, msg) }}
+ }
+
+ private fun sendMessage(app: GarminApplication, msg: Any) {
+ // Convert msg to string for logging.
+ val s = when (msg) {
+ is Map<*,*> ->
+ msg.entries.joinToString(", ", "(", ")") { (k, v) -> "$k=$v" }
+ is List<*> ->
+ msg.joinToString(", ", "(", ")")
+ else ->
+ msg.toString()
+ }
+ val data = GarminSerializer.serialize(msg)
+ aapsLogger.info(LTag.GARMIN, "sendMessage $app ${data.size} bytes $s")
+ try {
+ app.client.sendMessage(app, data)
+ } catch (e: IllegalStateException) {
+ aapsLogger.error(LTag.GARMIN, "${app.client} not connected", e)
+ }
+ }
+
+ override fun dispose() {
+ if (!disposed) {
+ clients.forEach { c -> c.dispose() }
+ disposed = true
+ }
+ clients.clear()
+ }
+
+ override fun isDisposed() = disposed
+}
\ No newline at end of file
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt
index 5d20fd3c70d..cc8661828f8 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt
@@ -1,5 +1,6 @@
package app.aaps.plugins.sync.garmin
+import android.content.Context
import androidx.annotation.VisibleForTesting
import app.aaps.core.interfaces.db.GlucoseUnit
import app.aaps.core.interfaces.logging.AAPSLogger
@@ -14,10 +15,15 @@ import app.aaps.core.interfaces.rx.events.EventPreferenceChange
import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.database.entities.GlucoseValue
import app.aaps.plugins.sync.R
+import com.google.gson.JsonArray
import com.google.gson.JsonObject
import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
+import java.math.BigDecimal
+import java.math.MathContext
+import java.math.RoundingMode
+import java.net.HttpURLConnection
import java.net.SocketAddress
import java.net.URI
import java.time.Clock
@@ -42,6 +48,7 @@ class GarminPlugin @Inject constructor(
injector: HasAndroidInjector,
aapsLogger: AAPSLogger,
resourceHelper: ResourceHelper,
+ private val context: Context,
private val loopHub: LoopHub,
private val rxBus: RxBus,
private val sp: SP,
@@ -57,7 +64,19 @@ class GarminPlugin @Inject constructor(
) {
/** HTTP Server for local HTTP server communication (device app requests values) .*/
private var server: HttpServer? = null
+ var garminMessenger: GarminMessenger? = null
+ /** Garmin ConnectIQ application id for native communication. Phone pushes values. */
+ private val glucoseAppIds = mapOf(
+ "C9E90EE7E6924829A8B45E7DAFFF5CB4" to "GlucoseWatch_Dev",
+ "1107CA6C2D5644B998D4BCB3793F2B7C" to "GlucoseDataField_Dev",
+ "928FE19A4D3A4259B50CB6F9DDAF0F4A" to "GlucoseWidget_Dev",
+ "662DFCF7F5A147DE8BD37F09574ADB11" to "GlucoseWatch",
+ "815C7328C21248C493AD9AC4682FE6B3" to "GlucoseDataField",
+ "4BDDCC1740084A1FAB83A3B2E2FCF55B" to "GlucoseWidget",
+ )
+
+ @VisibleForTesting
private val disposable = CompositeDisposable()
@VisibleForTesting
@@ -68,10 +87,24 @@ class GarminPlugin @Inject constructor(
var newValue: Condition = valueLock.newCondition()
private var lastGlucoseValueTimestamp: Long? = null
private val glucoseUnitStr get() = if (loopHub.glucoseUnit == GlucoseUnit.MGDL) "mgdl" else "mmoll"
+ private val garminAapsKey get() = sp.getString("garmin_aaps_key", "")
private fun onPreferenceChange(event: EventPreferenceChange) {
- aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}")
- setupHttpServer()
+ when (event.changedKey) {
+ "communication_debug_mode" -> setupGarminMessenger()
+ "communication_http", "communication_http_port" -> setupHttpServer()
+ "garmin_aaps_key" -> sendPhoneAppMessage()
+ }
+ }
+
+ private fun setupGarminMessenger() {
+ val enableDebug = sp.getBoolean("communication_ciq_debug_mode", false)
+ garminMessenger?.dispose()
+ garminMessenger = null
+ aapsLogger.info(LTag.GARMIN, "initialize IQ messenger in debug=$enableDebug")
+ garminMessenger = GarminMessenger(
+ aapsLogger, context, glucoseAppIds, {_, _ -> },
+ true, enableDebug).also { disposable.add(it) }
}
override fun onStart() {
@@ -83,19 +116,34 @@ class GarminPlugin @Inject constructor(
.observeOn(Schedulers.io())
.subscribe(::onPreferenceChange)
)
+ disposable.add(
+ rxBus
+ .toObservable(EventNewBG::class.java)
+ .observeOn(Schedulers.io())
+ .subscribe(::onNewBloodGlucose)
+ )
setupHttpServer()
+ if (garminAapsKey.isNotEmpty())
+ setupGarminMessenger()
}
private fun setupHttpServer() {
+ setupHttpServer(Duration.ZERO)
+ }
+
+ @VisibleForTesting
+ fun setupHttpServer(wait: Duration) {
if (sp.getBoolean("communication_http", false)) {
val port = sp.getInt("communication_http_port", 28891)
if (server != null && server?.port == port) return
aapsLogger.info(LTag.GARMIN, "starting HTTP server on $port")
server?.close()
server = HttpServer(aapsLogger, port).apply {
- registerEndpoint("/get", ::onGetBloodGlucose)
- registerEndpoint("/carbs", ::onPostCarbs)
- registerEndpoint("/connect", ::onConnectPump)
+ registerEndpoint("/get", requestHandler(::onGetBloodGlucose))
+ registerEndpoint("/carbs", requestHandler(::onPostCarbs))
+ registerEndpoint("/connect", requestHandler(::onConnectPump))
+ registerEndpoint("/sgv.json", requestHandler(::onSgv))
+ awaitReady(wait)
}
} else if (server != null) {
aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
@@ -104,7 +152,7 @@ class GarminPlugin @Inject constructor(
}
}
- override fun onStop() {
+ public override fun onStop() {
disposable.clear()
aapsLogger.info(LTag.GARMIN, "Stop")
server?.close()
@@ -128,6 +176,36 @@ class GarminPlugin @Inject constructor(
}
}
+ @VisibleForTesting
+ fun onConnectDevice(device: GarminDevice) {
+ if (garminAapsKey.isNotEmpty()) {
+ aapsLogger.info(LTag.GARMIN, "onConnectDevice $device sending glucose")
+ sendPhoneAppMessage(device)
+ }
+ }
+
+ private fun sendPhoneAppMessage(device: GarminDevice) {
+ garminMessenger?.sendMessage(device, getGlucoseMessage())
+ }
+
+ private fun sendPhoneAppMessage() {
+ garminMessenger?.sendMessage(getGlucoseMessage())
+ }
+
+ @VisibleForTesting
+ fun getGlucoseMessage() = mapOf(
+ "key" to garminAapsKey,
+ "command" to "glucose",
+ "profile" to loopHub.currentProfileName.first().toString(),
+ "encodedGlucose" to encodedGlucose(getGlucoseValues()),
+ "remainingInsulin" to loopHub.insulinOnboard,
+ "glucoseUnit" to glucoseUnitStr,
+ "temporaryBasalRate" to
+ (loopHub.temporaryBasal.takeIf(java.lang.Double::isFinite) ?: 1.0),
+ "connected" to loopHub.isConnected,
+ "timestamp" to clock.instant().epochSecond
+ )
+
/** Gets the last 2+ hours of glucose values. */
@VisibleForTesting
fun getGlucoseValues(): List {
@@ -161,21 +239,33 @@ class GarminPlugin @Inject constructor(
val glucoseMgDl: Int = glucose.value.roundToInt()
encodedGlucose.add(timeSec, glucoseMgDl)
}
- aapsLogger.info(
- LTag.GARMIN,
- "retrieved ${glucoseValues.size} last ${Date(glucoseValues.lastOrNull()?.timestamp ?: 0L)} ${encodedGlucose.size}"
- )
return encodedGlucose.encodedBase64()
}
+ @VisibleForTesting
+ fun requestHandler(action: (URI) -> CharSequence) = {
+ caller: SocketAddress, uri: URI, _: String? ->
+ val key = garminAapsKey
+ val deviceKey = getQueryParameter(uri, "key")
+ if (key.isNotEmpty() && key != deviceKey) {
+ aapsLogger.warn(LTag.GARMIN, "Invalid AAPS Key from $caller, got '$deviceKey' want '$key' $uri")
+ sendPhoneAppMessage()
+ Thread.sleep(1000L)
+ HttpURLConnection.HTTP_UNAUTHORIZED to "{}"
+ } else {
+ aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri")
+ HttpURLConnection.HTTP_OK to action(uri).also {
+ aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri, result: $it")
+ }
+ }
+ }
+
/** Responses to get glucose value request by the device.
*
* Also, gets the heart rate readings from the device.
*/
@VisibleForTesting
- @Suppress("UNUSED_PARAMETER")
- fun onGetBloodGlucose(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
- aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri")
+ fun onGetBloodGlucose(uri: URI): CharSequence {
receiveHeartRate(uri)
val profileName = loopHub.currentProfileName
val waitSec = getQueryParameter(uri, "wait", 0L)
@@ -189,9 +279,7 @@ class GarminPlugin @Inject constructor(
}
jo.addProperty("profile", profileName.first().toString())
jo.addProperty("connected", loopHub.isConnected)
- return jo.toString().also {
- aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri, result: $it")
- }
+ return jo.toString()
}
private fun getQueryParameter(uri: URI, name: String) = (uri.query ?: "")
@@ -223,6 +311,19 @@ class GarminPlugin @Inject constructor(
}
}
+ private fun toLong(v: Any?) = (v as? Number?)?.toLong() ?: 0L
+
+ @VisibleForTesting
+ fun receiveHeartRate(msg: Map, test: Boolean) {
+ val avg: Int = msg.getOrDefault("hr", 0) as Int
+ val samplingStartSec: Long = toLong(msg["hrStart"])
+ val samplingEndSec: Long = toLong(msg["hrEnd"])
+ val device: String? = msg["device"] as String?
+ receiveHeartRate(
+ Instant.ofEpochSecond(samplingStartSec), Instant.ofEpochSecond(samplingEndSec),
+ avg, device, test)
+ }
+
@VisibleForTesting
fun receiveHeartRate(uri: URI) {
val avg: Int = getQueryParameter(uri, "hr", 0L).toInt()
@@ -237,20 +338,18 @@ class GarminPlugin @Inject constructor(
private fun receiveHeartRate(
samplingStart: Instant, samplingEnd: Instant,
avg: Int, device: String?, test: Boolean) {
- aapsLogger.info(LTag.GARMIN, "average heart rate $avg BPM test=$test")
+ aapsLogger.info(LTag.GARMIN, "average heart rate $avg BPM $samplingStart to $samplingEnd")
if (test) return
if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) {
loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device)
- } else {
+ } else if (avg > 0) {
aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd")
}
}
/** Handles carb notification from the device. */
@VisibleForTesting
- @Suppress("UNUSED_PARAMETER")
- fun onPostCarbs(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
- aapsLogger.info(LTag.GARMIN, "carbs from $caller, req: $uri")
+ fun onPostCarbs(uri: URI): CharSequence {
postCarbs(getQueryParameter(uri, "carbs", 0L).toInt())
return ""
}
@@ -263,9 +362,7 @@ class GarminPlugin @Inject constructor(
/** Handles pump connected notification that the user entered on the Garmin device. */
@VisibleForTesting
- @Suppress("UNUSED_PARAMETER")
- fun onConnectPump(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
- aapsLogger.info(LTag.GARMIN, "connect from $caller, req: $uri")
+ fun onConnectPump(uri: URI): CharSequence {
val minutes = getQueryParameter(uri, "disconnectMinutes", 0L).toInt()
if (minutes > 0) {
loopHub.disconnectPump(minutes)
@@ -277,4 +374,61 @@ class GarminPlugin @Inject constructor(
jo.addProperty("connected", loopHub.isConnected)
return jo.toString()
}
+
+ private fun glucoseSlopeMgDlPerMilli(glucose1: GlucoseValue, glucose2: GlucoseValue): Double {
+ return (glucose2.value - glucose1.value) / (glucose2.timestamp - glucose1.timestamp)
+ }
+
+ /** Returns glucose values in Nightscout/Xdrip format. */
+ @VisibleForTesting
+ fun onSgv(uri: URI): CharSequence {
+ val count = getQueryParameter(uri,"count", 24L)
+ .toInt().coerceAtMost(1000).coerceAtLeast(1)
+ val briefMode = getQueryParameter(uri, "brief_mode", false)
+
+ // Guess a start time to get [count+1] readings. This is a heuristic that only works if we get readings
+ // every 5 minutes and we're not missing readings. We truncate in case we get more readings but we'll
+ // get less, e.g., in case we're missing readings for the last half hour. We get one extra reading,
+ // to compute the glucose delta.
+ val from = clock.instant().minus(Duration.ofMinutes(5L * (count + 1)))
+ val glucoseValues = loopHub.getGlucoseValues(from, false)
+ val joa = JsonArray()
+ for (i in 0 until count.coerceAtMost(glucoseValues.size)) {
+ val jo = JsonObject()
+ val glucose = glucoseValues[i]
+ if (!briefMode) {
+ jo.addProperty("_id", glucose.id.toString())
+ jo.addProperty("device", glucose.sourceSensor.toString())
+ val timestamp = Instant.ofEpochMilli(glucose.timestamp)
+ jo.addProperty("deviceString", timestamp.toString())
+ jo.addProperty("sysTime", timestamp.toString())
+ glucose.raw?.let { raw -> jo.addProperty("unfiltered", raw) }
+ }
+ jo.addProperty("date", glucose.timestamp)
+ jo.addProperty("sgv", glucose.value.roundToInt())
+ if (i + 1 < glucoseValues.size) {
+ // Compute the 5 minute delta.
+ val delta = 300_000.0 * glucoseSlopeMgDlPerMilli(glucoseValues[i + 1], glucose)
+ jo.addProperty("delta", BigDecimal(delta, MathContext(3, RoundingMode.HALF_UP)))
+ }
+ jo.addProperty("direction", glucose.trendArrow.text)
+ glucose.noise?.let { n -> jo.addProperty("noise", n) }
+ if (i == 0) {
+ when (loopHub.glucoseUnit) {
+ GlucoseUnit.MGDL -> jo.addProperty("units_hint", "mgdl")
+ GlucoseUnit.MMOL -> jo.addProperty("units_hint", "mmol")
+ }
+ jo.addProperty("iob", loopHub.insulinOnboard + loopHub.insulinBasalOnboard)
+ loopHub.temporaryBasal.also {
+ if (!it.isNaN()) {
+ val temporaryBasalRateInPercent = (it * 100.0).toInt()
+ jo.addProperty("tbr", temporaryBasalRateInPercent)
+ }
+ }
+ jo.addProperty("cob", loopHub.carbsOnboard)
+ }
+ joa.add(jo)
+ }
+ return joa.toString()
+ }
}
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt
new file mode 100644
index 00000000000..e8e93bac513
--- /dev/null
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt
@@ -0,0 +1,23 @@
+package app.aaps.plugins.sync.garmin
+
+/**
+ * Callback interface for a @see ConnectIqClient.
+ */
+interface GarminReceiver {
+ /**
+ * Notifies that the client is ready, i.e. the app client as bound to the Garmin
+ * Android app.
+ */
+ fun onConnect(client: GarminClient)
+ fun onDisconnect(client: GarminClient)
+
+ /**
+ * Delivers received device app messages.
+ */
+ fun onReceiveMessage(client: GarminClient, deviceId: Long, appId: String, data: ByteArray)
+
+ /**
+ * Delivers status of @see ConnectIqClient#sendMessage requests.
+ */
+ fun onSendMessage(client: GarminClient, deviceId: Long, appId: String, errorMessage: String?)
+}
\ No newline at end of file
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSerializer.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSerializer.kt
new file mode 100644
index 00000000000..495278e9dab
--- /dev/null
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSerializer.kt
@@ -0,0 +1,254 @@
+package app.aaps.plugins.sync.garmin
+
+import java.io.ByteArrayOutputStream
+import java.io.DataOutputStream
+import java.nio.ByteBuffer
+import java.util.ArrayDeque
+import java.util.Queue
+
+/**
+ * Serialize and Deserialize objects in Garmin format.
+ *
+ * Format is as follows:
+ * ...
+ *
+ * Serialized data starts with an optional string block. The string block is preceded with the STRS_MARKER,
+ * followed by the total length of the reminder (4 bytes). Then foreach string, the string length
+ * (2 bytes), followed by the string bytes, followed by a \0 byte.
+ *
+ * Objects are stored starting with OBJS_MARKER, followed by the total length (4 bytes), followed
+ * by a flat list of objects. Each object starts with its type (1 byte), followed by the data
+ * for numbers in Boolean. Strings a represented by an index into the string block. Arrays only have
+ * the length, the actual objects will be in the list of objects. Similarly, maps only have the
+ * length and the entries are represented by 2 objects (key + val) in the list of objects.
+ */
+object GarminSerializer {
+ private const val NULL = 0
+ private const val INT = 1
+ private const val FLOAT = 2
+ private const val STRING = 3
+ private const val ARRAY = 5
+ private const val BOOLEAN = 9
+ private const val MAP = 11
+ private const val LONG = 14
+ private const val DOUBLE = 15
+ private const val CHAR = 19
+
+ private const val STRINGS_MARKER = -1412584499
+ private const val OBJECTS_MARKER = -629482886
+ // ArrayDeque doesn't like null so we use this instead.
+ private val NULL_MARKER = object {}
+
+ private interface Container {
+ fun read(buf: ByteBuffer, strings: Map, container: Queue)
+ }
+
+ private class ListContainer(
+ val size: Int,
+ val list: MutableList
+ ) : Container {
+
+ override fun read(buf: ByteBuffer, strings: Map, container: Queue) {
+ for (i in 0 until size) {
+ list.add(readObject(buf, strings, container))
+ }
+ }
+ }
+
+ private class MapContainer(
+ val size: Int,
+ val map: MutableMap
+ ) : Container {
+
+ override fun read(buf: ByteBuffer, strings: Map, container: Queue) {
+ for (i in 0 until size) {
+ val k = readObject(buf, strings, container)
+ val v = readObject(buf, strings, container)
+ map[k!!] = v
+ }
+ }
+ }
+
+
+ fun serialize(obj: Any?): ByteArray {
+ val strsOut = ByteArrayOutputStream()
+ val strsDataOut = DataOutputStream(strsOut)
+ val objsOut = ByteArrayOutputStream()
+ val strings = mutableMapOf()
+ val q = ArrayDeque()
+
+ q.add(obj ?: NULL_MARKER)
+ while (!q.isEmpty()) {
+ serialize(q.poll(), strsDataOut, DataOutputStream(objsOut), strings, q)
+ }
+
+ var bufLen = 8 + objsOut.size()
+ if (strsOut.size() > 0) {
+ bufLen += 8 + strsOut.size()
+ }
+
+ val buf = ByteBuffer.allocate(bufLen)
+ if (strsOut.size() > 0) {
+ buf.putInt(STRINGS_MARKER)
+ buf.putInt(strsOut.size())
+ buf.put(strsOut.toByteArray(), 0, strsOut.size())
+ }
+ buf.putInt(OBJECTS_MARKER)
+ buf.putInt(objsOut.size())
+ buf.put(objsOut.toByteArray(), 0, objsOut.size())
+ return buf.array()
+ }
+
+ private fun serialize(
+ obj: Any?,
+ strOut: DataOutputStream,
+ objOut: DataOutputStream,
+ strings: MutableMap,
+ q: Queue
+ ) {
+ when (obj) {
+ NULL_MARKER -> objOut.writeByte(NULL)
+
+ is Int -> {
+ objOut.writeByte(INT)
+ objOut.writeInt(obj)
+ }
+
+ is Float -> {
+ objOut.writeByte(FLOAT)
+ objOut.writeFloat(obj)
+ }
+
+ is String -> {
+ objOut.writeByte(STRING)
+ val offset = strings[obj]
+ if (offset == null) {
+ strings[obj] = strOut.size()
+ val bytes = obj.toByteArray(Charsets.UTF_8)
+ strOut.writeShort(bytes.size + 1)
+ strOut.write(bytes)
+ strOut.write(0)
+ }
+ objOut.writeInt(strings[obj]!!)
+ }
+
+ is List<*> -> {
+ objOut.writeByte(ARRAY)
+ objOut.writeInt(obj.size)
+ obj.forEach { o -> q.add(o ?: NULL_MARKER) }
+ }
+
+ is Boolean -> {
+ objOut.writeByte(BOOLEAN)
+ objOut.writeByte(if (obj) 1 else 0)
+ }
+
+ is Map<*, *> -> {
+ objOut.writeByte(MAP)
+ objOut.writeInt(obj.size)
+ obj.entries.forEach { (k, v) ->
+ q.add(k ?: NULL_MARKER); q.add(v ?: NULL_MARKER) }
+ }
+
+ is Long -> {
+ objOut.writeByte(LONG)
+ objOut.writeLong(obj)
+ }
+
+ is Double -> {
+ objOut.writeByte(DOUBLE)
+ objOut.writeDouble(obj)
+ }
+
+ is Char -> {
+ objOut.writeByte(CHAR)
+ objOut.writeInt(obj.code)
+ }
+
+ else ->
+ throw IllegalArgumentException("Unsupported type ${obj?.javaClass} '$obj'")
+ }
+ }
+
+ fun deserialize(data: ByteArray): Any? {
+ val buf = ByteBuffer.wrap(data)
+ val marker1 = buf.getInt(0)
+ val strings = if (marker1 == STRINGS_MARKER) {
+ buf.int // swallow the marker
+ readStrings(buf)
+ } else {
+ emptyMap()
+ }
+ val marker2 = buf.int // swallow the marker
+ if (marker2 != OBJECTS_MARKER) {
+ throw IllegalArgumentException("expected data marker, got $marker2")
+ }
+ return readObjects(buf, strings)
+ }
+
+ private fun readStrings(buf: ByteBuffer): Map {
+ val strings = mutableMapOf()
+ val strBufferLen = buf.int
+ val offset = buf.position()
+ while (buf.position() - offset < strBufferLen) {
+ val pos = buf.position() - offset
+ val strLen = buf.short.toInt() - 1 // ignore \0 byte
+ val strBytes = ByteArray(strLen)
+ buf.get(strBytes)
+ strings[pos] = String(strBytes, Charsets.UTF_8)
+ buf.get() // swallow \0 byte
+ }
+ return strings
+ }
+
+ private fun readObjects(buf: ByteBuffer, strings: Map): Any? {
+ val objBufferLen = buf.int
+ if (objBufferLen > buf.remaining()) {
+ throw IllegalArgumentException("expect $objBufferLen bytes got ${buf.remaining()}")
+ }
+
+ val container = ArrayDeque()
+ val r = readObject(buf, strings, container)
+ while (container.isNotEmpty()) {
+ container.pollFirst()?.read(buf, strings, container)
+ }
+
+ return r
+ }
+
+ private fun readObject(buf: ByteBuffer, strings: Map, q: Queue): Any? {
+ when (buf.get().toInt()) {
+ NULL -> return null
+ INT -> return buf.int
+ FLOAT -> return buf.float
+
+ STRING -> {
+ val offset = buf.int
+ return strings[offset]!!
+ }
+
+ ARRAY -> {
+ val arraySize = buf.int
+ val array = mutableListOf()
+ // We will populate the array with arraySize objects from the object list later,
+ // when we take the ListContainer from the queue.
+ q.add(ListContainer(arraySize, array))
+ return array
+ }
+
+ BOOLEAN -> return buf.get() > 0
+
+ MAP -> {
+ val mapSize = buf.int
+ val map = mutableMapOf()
+ q.add(MapContainer(mapSize, map))
+ return map
+ }
+
+ LONG -> return buf.long
+ DOUBLE -> return buf.double
+ CHAR -> return Char(buf.int)
+ else -> return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt
new file mode 100644
index 00000000000..01712a8da6e
--- /dev/null
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt
@@ -0,0 +1,182 @@
+package app.aaps.plugins.sync.garmin
+
+import app.aaps.core.interfaces.logging.AAPSLogger
+import app.aaps.core.interfaces.logging.LTag
+import com.garmin.android.connectiq.IQApp
+import com.garmin.android.connectiq.IQApp.IQAppStatus
+import io.reactivex.rxjava3.disposables.Disposable
+import org.jetbrains.annotations.VisibleForTesting
+import java.io.InputStream
+import java.net.Inet4Address
+import java.net.InetSocketAddress
+import java.net.ServerSocket
+import java.net.Socket
+import java.net.SocketException
+import java.time.Duration
+import java.time.Instant
+import java.util.Collections
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicLong
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/** [GarminClient] that talks to the ConnectIQ simulator via HTTP.
+ *
+ * This is needed for Garmin device app development. */
+class GarminSimulatorClient(
+ private val aapsLogger: AAPSLogger,
+ private val receiver: GarminReceiver,
+ var port: Int = 7381
+): Disposable, GarminClient {
+
+ override val name = "Sim"
+ private val executor: ExecutorService = Executors.newCachedThreadPool()
+ private val serverSocket = ServerSocket()
+ private val connections: MutableList = Collections.synchronizedList(mutableListOf())
+ private var nextDeviceId = AtomicLong(1)
+ @VisibleForTesting
+ val iqApp = IQApp("SimApp", IQAppStatus.INSTALLED, "Simulator", 1)
+ private val readyLock = ReentrantLock()
+ private val readyCond = readyLock.newCondition()
+ override val connectedDevices: List get() = connections.map { c -> c.device }
+
+ override fun registerForMessages(app: GarminApplication) {
+ }
+
+ private inner class Connection(private val socket: Socket): Disposable {
+ val device = GarminDevice(
+ this@GarminSimulatorClient,
+ nextDeviceId.getAndAdd(1L),
+ "Sim@${socket.remoteSocketAddress}")
+
+ fun start() {
+ executor.execute {
+ try {
+ run()
+ } catch (e: Throwable) {
+ aapsLogger.error(LTag.GARMIN, "$device failed", e)
+ }
+ }
+ }
+
+ fun send(data: ByteArray) {
+ if (socket.isConnected && !socket.isOutputShutdown) {
+ aapsLogger.info(LTag.GARMIN, "sending ${data.size} bytes to $device")
+ socket.outputStream.write(data)
+ socket.outputStream.flush()
+ } else {
+ aapsLogger.warn(LTag.GARMIN, "socket closed, cannot send $device")
+ }
+ }
+
+ private fun run() {
+ socket.soTimeout = 0
+ socket.isInputShutdown
+ while (!socket.isClosed && socket.isConnected) {
+ try {
+ val data = readAvailable(socket.inputStream) ?: break
+ if (data.isNotEmpty()) {
+ kotlin.runCatching {
+ receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationId, data)
+ }
+ }
+ } catch (e: SocketException) {
+ aapsLogger.warn(LTag.GARMIN, "socket read failed ${e.message}")
+ break
+ }
+ }
+ aapsLogger.info(LTag.GARMIN, "disconnect ${device.name}" )
+ connections.remove(this)
+ }
+
+ private fun readAvailable(input: InputStream): ByteArray? {
+ val buffer = ByteArray(1 shl 14)
+ aapsLogger.info(LTag.GARMIN, "$device reading")
+ val len = input.read(buffer)
+ aapsLogger.info(LTag.GARMIN, "$device read $len bytes")
+ if (len < 0) {
+ return null
+ }
+ val data = ByteArray(len)
+ System.arraycopy(buffer, 0, data, 0, data.size)
+ return data
+ }
+
+ override fun dispose() {
+ aapsLogger.info(LTag.GARMIN, "close $device")
+
+ @Suppress("EmptyCatchBlock")
+ try {
+ socket.close()
+ } catch (e: SocketException) {
+ aapsLogger.warn(LTag.GARMIN, "closing socket failed ${e.message}")
+ }
+ }
+
+ override fun isDisposed() = socket.isClosed
+ }
+
+ init {
+ executor.execute {
+ runCatching(::listen).exceptionOrNull()?.let { e->
+ aapsLogger.error(LTag.GARMIN, "listen failed", e)
+ }
+ }
+ }
+
+ private fun listen() {
+ val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1))
+ aapsLogger.info(LTag.GARMIN, "bind to $ip:$port")
+ serverSocket.bind(InetSocketAddress(ip, port))
+ port = serverSocket.localPort
+ receiver.onConnect(this@GarminSimulatorClient)
+ while (!serverSocket.isClosed) {
+ val s = serverSocket.accept()
+ aapsLogger.info(LTag.GARMIN, "accept " + s.remoteSocketAddress)
+ connections.add(Connection(s))
+ connections.last().start()
+ }
+ receiver.onDisconnect(this@GarminSimulatorClient)
+ }
+
+ /** Wait for the server to start listing to requests. */
+ fun awaitReady(wait: Duration): Boolean {
+ val waitUntil = Instant.now() + wait
+ readyLock.withLock {
+ while (!serverSocket.isBound && Instant.now() < waitUntil) {
+ readyCond.await(20, TimeUnit.MILLISECONDS)
+ }
+ }
+ return serverSocket.isBound
+ }
+
+ override fun dispose() {
+ connections.forEach { c -> c.dispose() }
+ connections.clear()
+ serverSocket.close()
+ executor.awaitTermination(10, TimeUnit.SECONDS)
+ }
+
+ override fun isDisposed() = serverSocket.isClosed
+
+ private fun getConnection(device: GarminDevice): Connection? {
+ return connections.firstOrNull { c -> c.device.id == device.id }
+ }
+
+ override fun sendMessage(app: GarminApplication, data: ByteArray) {
+ val c = getConnection(app.device) ?: return
+ try {
+ c.send(data)
+ receiver.onSendMessage(this, app.device.id, app.id, null)
+ } catch (e: SocketException) {
+ val errorMessage = "sending failed '${e.message}'"
+ receiver.onSendMessage(this, app.device.id, app.id, errorMessage)
+ c.dispose()
+ connections.remove(c)
+ }
+ }
+
+ override fun toString() = name
+}
\ No newline at end of file
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt
index c349e3cb3d8..82f2e0806d7 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt
@@ -34,7 +34,7 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po
private val serverThread: Thread
private val workerExecutor: Executor = Executors.newCachedThreadPool()
- private val endpoints: MutableMap CharSequence> =
+ private val endpoints: MutableMap Pair> =
ConcurrentHashMap()
private var serverSocket: ServerSocket? = null
private val readyLock = ReentrantLock()
@@ -76,7 +76,7 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po
}
/** Register an endpoint (path) to handle requests. */
- fun registerEndpoint(path: String, endpoint: (SocketAddress, URI, String?) -> CharSequence) {
+ fun registerEndpoint(path: String, endpoint: (SocketAddress, URI, String?) -> Pair) {
aapsLogger.info(LTag.GARMIN, "Register: '$path'")
endpoints[path] = endpoint
}
@@ -127,8 +127,8 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po
respond(HttpURLConnection.HTTP_NOT_FOUND, out)
} else {
try {
- val body = endpoint(s.remoteSocketAddress, uri, reqBody)
- respond(HttpURLConnection.HTTP_OK, body, "application/json", out)
+ val (code, body) = endpoint(s.remoteSocketAddress, uri, reqBody)
+ respond(code, body, "application/json", out)
} catch (e: Exception) {
aapsLogger.error(LTag.GARMIN, "endpoint " + uri.path + " failed", e)
respond(HttpURLConnection.HTTP_INTERNAL_ERROR, out)
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt
index 4b420d21f5d..ebd8ac209b8 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt
@@ -20,6 +20,12 @@ interface LoopHub {
/** Returns the remaining bolus insulin on board. */
val insulinOnboard: Double
+ /** Returns the basal insulin on board. */
+ val insulinBasalOnboard: Double
+
+ /** Returns the remaining carbs on board. */
+ val carbsOnboard: Double?
+
/** Returns true if the pump is connected. */
val isConnected: Boolean
@@ -48,4 +54,4 @@ interface LoopHub {
avgHeartRate: Int,
device: String?
)
-}
\ No newline at end of file
+}
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt
index dfcf9dab376..1e9f19ae7fd 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt
@@ -13,6 +13,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction
import app.aaps.core.interfaces.pump.DetailedBolusInfo
import app.aaps.core.interfaces.queue.CommandQueue
import app.aaps.core.interfaces.sharedPreferences.SP
+import app.aaps.core.main.graph.OverviewData
import app.aaps.database.ValueWrapper
import app.aaps.database.entities.EffectiveProfileSwitch
import app.aaps.database.entities.GlucoseValue
@@ -42,6 +43,7 @@ class LoopHubImpl @Inject constructor(
private val repo: AppRepository,
private val userEntryLogger: UserEntryLogger,
private val sp: SP,
+ private val overviewData: OverviewData,
) : LoopHub {
@VisibleForTesting
@@ -64,6 +66,14 @@ class LoopHubImpl @Inject constructor(
override val insulinOnboard: Double
get() = iobCobCalculator.calculateIobFromBolus().iob
+ /** Returns the remaining bolus and basal insulin on board. */
+ override val insulinBasalOnboard :Double
+ get() = iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().basaliob
+
+ /** Returns the remaining carbs on board. */
+ override val carbsOnboard: Double?
+ get() = overviewData.cobInfo(iobCobCalculator).displayCob
+
/** Returns true if the pump is connected. */
override val isConnected: Boolean get() = !loop.isDisconnected
@@ -142,4 +152,4 @@ class LoopHubImpl @Inject constructor(
)
repo.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait()
}
-}
\ No newline at end of file
+}
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsShared/NsIncomingDataProcessor.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsShared/NsIncomingDataProcessor.kt
index 96c6a65c552..9806d665a7e 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsShared/NsIncomingDataProcessor.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsShared/NsIncomingDataProcessor.kt
@@ -88,6 +88,8 @@ class NsIncomingDataProcessor @Inject constructor(
*/
@Suppress("SpellCheckingInspection")
fun processSgvs(sgvs: Any): Boolean {
+ // Objective0
+ sp.putBoolean(app.aaps.core.utils.R.string.key_objectives_bg_is_available_in_ns, true)
if (!nsClientSource.isEnabled() && !sp.getBoolean(app.aaps.core.utils.R.string.key_ns_receive_cgm, false)) return false
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/services/NSClientService.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/services/NSClientService.kt
index bac58bc0ed9..adf08f21907 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/services/NSClientService.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/services/NSClientService.kt
@@ -561,8 +561,6 @@ class NSClientService : DaggerService() {
val sgvs = data.getJSONArray("sgvs")
if (sgvs.length() > 0) {
rxBus.send(EventNSClientNewLog("◄ DATA", "received " + sgvs.length() + " sgvs"))
- // Objective0
- sp.putBoolean(app.aaps.core.utils.R.string.key_objectives_bg_is_available_in_ns, true)
nsIncomingDataProcessor.processSgvs(sgvs)
storeDataForDb.storeGlucoseValuesToDb()
}
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt
index 27a8bb73665..cf0404812ac 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt
@@ -167,7 +167,8 @@ class NSClientV3Service : DaggerService() {
rxBus.send(EventNSClientNewLog("◄ WS", "Subscribed for: ${response.optString("collections")}"))
// during disconnection updated data is not received
// thus run non WS load to get missing data
- nsClientV3Plugin.executeLoop("WS_CONNECT", forceNew = false)
+ nsClientV3Plugin.initialLoadFinished = false
+ nsClientV3Plugin.executeLoop("WS_CONNECT", forceNew = true)
true
} else {
rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed"))
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorker.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorker.kt
index 5e2976c9b43..7f2882e529e 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorker.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorker.kt
@@ -61,8 +61,6 @@ class LoadBgWorker(
if (sgvs.isNotEmpty()) {
val action = if (isFirstLoad) "RCV-F" else "RCV"
rxBus.send(EventNSClientNewLog("◄ $action", "${sgvs.size} SVGs from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}"))
- // Objective0
- sp.putBoolean(app.aaps.core.utils.R.string.key_objectives_bg_is_available_in_ns, true)
// Schedule processing of fetched data and continue of loading
continueLoading = response.code != 304 && nsIncomingDataProcessor.processSgvs(sgvs)
} else {
diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt
similarity index 97%
rename from plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt
rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt
index 6ee7128b169..3f90c172c9a 100644
--- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt
+++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt
@@ -1,4 +1,4 @@
-package app.aaps.plugins.sync.dataBroadcaster
+package app.aaps.plugins.sync.tizen
import android.content.Context
import android.content.Intent
@@ -41,7 +41,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class DataBroadcastPlugin @Inject constructor(
+class TizenPlugin @Inject constructor(
injector: HasAndroidInjector,
aapsLogger: AAPSLogger,
rh: ResourceHelper,
@@ -64,9 +64,9 @@ class DataBroadcastPlugin @Inject constructor(
PluginDescription()
.mainType(PluginType.SYNC)
.pluginIcon(app.aaps.core.main.R.drawable.ic_watch)
- .pluginName(R.string.data_broadcaster)
- .shortName(R.string.data_broadcaster_short)
- .description(R.string.data_broadcaster_description),
+ .pluginName(R.string.tizen)
+ .shortName(R.string.tizen_short)
+ .description(R.string.tizen_description),
aapsLogger, rh, injector
) {
diff --git a/plugins/sync/src/main/res/values-cs-rCZ/strings.xml b/plugins/sync/src/main/res/values-cs-rCZ/strings.xml
index 870a05b4af0..e557bea9986 100644
--- a/plugins/sync/src/main/res/values-cs-rCZ/strings.xml
+++ b/plugins/sync/src/main/res/values-cs-rCZ/strings.xml
@@ -117,8 +117,9 @@
Poslat data o glykémii a ošetření do xDrip+. Musí být vybrán zdroj dat \"xDrip+ Sync Follower\" a přijímání dat musí být povoleno v Nastavení - Nastavení komunikace mezi aplikacemi - Přijímat Glykémie/Ošetření
Povolit odesílání do xDrip+.
- DBRO
- Odesílání dat do Garmin aplikace G-Watch Wear App
+ Samsung Tizen
+ TIZ
+ Odesílání dat do Samsung aplikace G-Watch Wear App (Tizen OS)
Garmin
Připojení k zařízení Garmin (Fenix, Edge, …)
diff --git a/plugins/sync/src/main/res/values-es-rES/strings.xml b/plugins/sync/src/main/res/values-es-rES/strings.xml
index 05ce3e3902f..4cc1e18c357 100644
--- a/plugins/sync/src/main/res/values-es-rES/strings.xml
+++ b/plugins/sync/src/main/res/values-es-rES/strings.xml
@@ -117,8 +117,6 @@
Enviar datos de glucosa y tratamientos a xDrip+. La fuente de datos \"xDrip+ Sync Follower\" debe estar seleccionada y la aceptación de datos debe estar activada en Ajustes - Ajustes entre aplicaciones - Aceptar glucosa/tratamientos
Activar las transmisiones a xDrip+
- DBRO
- Transmitir datos a la aplicación G-Watch Wear de Garmin
Garmin
Conexión al dispositivo Garmin (Fénix, Edge, …)
@@ -128,7 +126,7 @@
Supervisar y controlar AAPS usando un reloj WearOS
(Ningún reloj conectado)
Estado de la bomba de insulina
- Estado del lazo
+ Estado del bucle
Calc. Asistente:\nInsulina: %1$.2fU\nCarbohidratos: %2$dg
El asistente rápido seleccionado ya no está disponible, por favor actualice su tarjeta
Asistente Rápido: %1$s\nInsulina: %2$.2fU\nCarbohidratos: %3$dg
diff --git a/plugins/sync/src/main/res/values-fr-rFR/strings.xml b/plugins/sync/src/main/res/values-fr-rFR/strings.xml
index 2af445a7287..9607d003ad7 100644
--- a/plugins/sync/src/main/res/values-fr-rFR/strings.xml
+++ b/plugins/sync/src/main/res/values-fr-rFR/strings.xml
@@ -17,6 +17,7 @@
Synchronise vos données avec Nightscout en utilisant l\'API v3
Bloqué par les options de recharge
Bloqué par les options de connectivité
+ Aucune connectivité
Version de Nightscout non supportée
OAPS
UPLD
@@ -116,9 +117,6 @@
Envoyer les glycémies et les traitements à xDrip+. La source de données \"xDrip+ Sync Follower\" doit être sélectionnée et l\'acceptation des données doit être activée dans Paramètres - Paramètres Inter-app - Accepter Glycémies/Traitements
Activer les diffusions vers xDrip+.
- Diffuseur de données
- DD
- Diffuser des données sur l\'application G-Watch Wear de Garmin
Garmin
Connexion au périphérique Garmin (Fenix, Edge, …)
diff --git a/plugins/sync/src/main/res/values-it-rIT/strings.xml b/plugins/sync/src/main/res/values-it-rIT/strings.xml
index 1e821f52994..91bec164964 100644
--- a/plugins/sync/src/main/res/values-it-rIT/strings.xml
+++ b/plugins/sync/src/main/res/values-it-rIT/strings.xml
@@ -117,8 +117,9 @@
Invia dati glicemia e trattamenti a xDrip+. La sorgente dati \"xDrip+ Sync Follower\" deve essere selezionata e l\'accettazione dei dati deve essere abilitata in: Settings - Inter-app settings - Accept Glucose/Treatments
Abilita trasmissioni a xDrip+.
- DBRO
- Trasmetti i dati all\'app G-Watch Wear di Garmin
+ Samsung Tizen
+ TIZ
+ Trasmetti i dati all\'app G-Watch Wear di Samsung (Tizen OS)
Garmin
Connessione al dispositivo Garmin (Fenix, Edge, …)
diff --git a/plugins/sync/src/main/res/values-lt-rLT/strings.xml b/plugins/sync/src/main/res/values-lt-rLT/strings.xml
index fd27843caf1..c06aae11d44 100644
--- a/plugins/sync/src/main/res/values-lt-rLT/strings.xml
+++ b/plugins/sync/src/main/res/values-lt-rLT/strings.xml
@@ -117,7 +117,6 @@
Siųsti glikemijos ir terapijos duomenis į xDrip+. Turi būti pasirinktas duomenų šaltinis „xDrip+ Sync Follower“, o duomenų priėmimas turi būti įjungtas skiltyje Nustatymai – Programų sąveikos nustatymai – Priimti glikemiją/terapiją
Įjungti duomenų perdavimą į xDrip+.
- Data Broadcaster
Garmin
Susiejimas su Garmin (Fenix, Edge, …)
diff --git a/plugins/sync/src/main/res/values-nb-rNO/strings.xml b/plugins/sync/src/main/res/values-nb-rNO/strings.xml
index 0e99568379d..7af2272a926 100644
--- a/plugins/sync/src/main/res/values-nb-rNO/strings.xml
+++ b/plugins/sync/src/main/res/values-nb-rNO/strings.xml
@@ -117,8 +117,9 @@
Send data om glukose og behandling til xDrip+. Velg datakilde \"xDrip+ Sync Følger\" og aktiver mottak av data under Innstillinger - Inter-app innstillinger - Aksepter glukose/behandlinger
Aktiver sending til xDrip+.
- DBRO
- Kringkast data til Garmin\'s G-Watch Wear app
+ Samsung Tizen
+ TIZ
+ Kringkast data til Samsung\'s G-Watch Wear app (Tizen OS)
Garmin
Tilkobling til Garmin-enheter (Fenix, Edge,…)
diff --git a/plugins/sync/src/main/res/values-nl-rNL/strings.xml b/plugins/sync/src/main/res/values-nl-rNL/strings.xml
index 477feec8a6f..fff5b226341 100644
--- a/plugins/sync/src/main/res/values-nl-rNL/strings.xml
+++ b/plugins/sync/src/main/res/values-nl-rNL/strings.xml
@@ -117,8 +117,6 @@
Verzend glucose en behandelingsgegevens naar xDrip+. Gegevensbron \"xDrip+ Sync Follower\" moet worden geselecteerd en het accepteren van gegevens moet worden ingeschakeld in Instellingen - Inter-app instellingen - Accepteer Glucose/Behandelingen
Activeer uitzendingen naar xDrip+.
- DBRO
- Verzend gegevens naar Garmin\'s G-Watch Wear App
Garmin
Verbinding met Garmin apparaat (Fenix, Edge, …)
diff --git a/plugins/sync/src/main/res/values-pl-rPL/strings.xml b/plugins/sync/src/main/res/values-pl-rPL/strings.xml
index d444294137b..22043712ab6 100644
--- a/plugins/sync/src/main/res/values-pl-rPL/strings.xml
+++ b/plugins/sync/src/main/res/values-pl-rPL/strings.xml
@@ -117,8 +117,6 @@
Wyślij dane dotyczące glikemii i leczenia do xDrip+. W ustawieniach xDrip+ należy ustawić \"Sprzętowe źródło danych\" na \"xDrip+ Sync Follower\" oraz włączyć akceptowanie danych: \"Ustawienia innych aplikacji\" - \"Akceptuj Glukozę/Akceptuj zabiegi\"
Włącz nadawanie do xDrip+.
- DBRO
- Transmisja danych do aplikacji G-Watch Garmin
Garmin
Połączenie z urządzeniem Garmin (Fenix, Edge, …)
diff --git a/plugins/sync/src/main/res/values-ro-rRO/strings.xml b/plugins/sync/src/main/res/values-ro-rRO/strings.xml
index 712f43398e6..841bfeff1e9 100644
--- a/plugins/sync/src/main/res/values-ro-rRO/strings.xml
+++ b/plugins/sync/src/main/res/values-ro-rRO/strings.xml
@@ -117,8 +117,9 @@
Trimite date despre glucoză și tratamente către xDrip+. Trebuie să fie selectată sursa de date \"Sincronizare xDrip+ Urmăritor\" și acceptarea datelor trebuie să fie activată în „Setări - Setări între aplicații - Acceptă Glucoză/Tratamente”
Activează transmisiuni spre xDrip+.
- DBRO
- Transmitere date către aplicaţia G-Watch Wear pe Garmin
+ Samsung Tizen
+ TIZ
+ Transmitere de date către aplicaţia Samsung G-Watch Wear (TizenOS)
Garmin
Conexiune la dispozitivul Garmin (Fenix, Edge, …)
diff --git a/plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml b/plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml
index 03f89ca280a..b497eb4703c 100644
--- a/plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml
+++ b/plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml
@@ -22,7 +22,7 @@
Условия пользования
Внимательно ознакомьтесь со следующей информацией и примите условия использования.
Это проект с открытым исходным кодом, который будет копировать ваши данные на Open Humans. Мы не будем обмениваться вашими данными с третьими лицами без вашего явного разрешения. Данные, отправляемые на проект и приложение, идентифицируются с помощью случайного идентификатора и будут безопасно передаваться на учетную запись Open Humans при вашем одобрении этого процесса. Вы можете остановить загрузку и удалить загруженные данные в любое время здесь: www.openhumans.org.
- Данные загружены
+ Загружаемые данные
Гликемия
Болюсы
Пролонгированные болюсы
@@ -37,7 +37,7 @@
Модель устройства
Размеры экрана
Данные отладки алгоритма
- Данные НЕ загружены
+ НЕзагружаемые данные
Пароли
URL-адрес Nightscout
Секретный ключ Nightscout API
diff --git a/plugins/sync/src/main/res/values-ru-rRU/strings.xml b/plugins/sync/src/main/res/values-ru-rRU/strings.xml
index a2498c8e089..b3daffd9c00 100644
--- a/plugins/sync/src/main/res/values-ru-rRU/strings.xml
+++ b/plugins/sync/src/main/res/values-ru-rRU/strings.xml
@@ -117,8 +117,9 @@
Отправлять данные о глюкозе и терапии на xDrip+. Источником данных должен быть выбран \"xDrip+ Sync Follower\" а в настройках между приложениями надо включить - Принимать глюкозу/терапию
Включить трансляции для xDrip+.
- DBRO
- Передавать данные приложению Garmin\'s G-Watch Wear
+ Samsung Tizen
+ TIZ
+ Передавать данные приложению Samsung G-Watch Wear (OS Tizen)
Garmin
Подключение к устройству Garmin (Fenix, Edge, …)
diff --git a/plugins/sync/src/main/res/values-sk-rSK/strings.xml b/plugins/sync/src/main/res/values-sk-rSK/strings.xml
index b4a49e688ee..798a6d17f16 100644
--- a/plugins/sync/src/main/res/values-sk-rSK/strings.xml
+++ b/plugins/sync/src/main/res/values-sk-rSK/strings.xml
@@ -17,6 +17,7 @@
Synchronizuje vaše dáta s Nightscoutom pomocou v3 API
Zablokované možnosti nabíjania
Zablokované možnosti pripojenia
+ Žiadne pripojenie
Nepodporovaná verzia Nighscoutu
OAPS
UPLD
@@ -116,8 +117,9 @@
Poslať dáta o glykémii a ošetrení do xDrip+. Musí byť vybraný zdroj dát \"xDrip+ Sync Follower\" a prijímanie dát musí byť povolené v Nastavenia - Nastavenie komunikácie medzi aplikáciami - Prijímať Glykémie/Ošetrenia
Povoliť odosielanie do xDrip+.
- DBRO
- Odosielanie dát do Garmin aplikácie G-Watch Wear
+ Samsung Tizen
+ TIZ
+ Odosielanie dát do Samsung aplikácie G-Watch Wear App (Tizen OS)
Garmin
Pripájanie k zariadeniu Garmin (Fénix, Edge, ...)
diff --git a/plugins/sync/src/main/res/values-tr-rTR/strings.xml b/plugins/sync/src/main/res/values-tr-rTR/strings.xml
index c90441313e8..8f02ae2630a 100644
--- a/plugins/sync/src/main/res/values-tr-rTR/strings.xml
+++ b/plugins/sync/src/main/res/values-tr-rTR/strings.xml
@@ -117,8 +117,9 @@
KŞ ve tedavi verilerini xDrip+\'a gönderin. Veri Kaynağı \"xDrip+ Sync Follower\" seçilmeli ve Ayarlar - Uygulamalar arası ayarlar - KŞ/Tedavileri Kabul Et bölümünde verilerin kabul edilmesi etkinleştirilmelidir.
xDrip+ \'a yayınları etkinleştirin.
- DBRO
- Verileri Garmin\'in G-Watch Wear Uygulamasına yayınlayın
+ Samsung Tizen
+ TIZ
+ Verileri Samsung\'un G-Watch Wear Uygulamasına (Tizen OS) yayınlayın
Garmin
Garmin cihazına bağlantı (Fenix, Edge,…)
diff --git a/plugins/sync/src/main/res/values/strings.xml b/plugins/sync/src/main/res/values/strings.xml
index 9a85f46131f..266db555304 100644
--- a/plugins/sync/src/main/res/values/strings.xml
+++ b/plugins/sync/src/main/res/values/strings.xml
@@ -180,9 +180,9 @@
- Data Broadcaster
- DBRO
- Broadcast data to Garmin\'s G-Watch Wear App
+ Samsung Tizen
+ TIZ
+ Broadcast data to Samsung\'s G-Watch Wear App (Tizen OS)
Garmin
diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt
new file mode 100644
index 00000000000..3497e4c9008
--- /dev/null
+++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt
@@ -0,0 +1,110 @@
+package app.aaps.plugins.sync.garmin
+
+import android.content.Context
+import app.aaps.shared.tests.TestBase
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+
+class GarminMessengerTest: TestBase() {
+ private val context = mock()
+
+ private var appId1 = "appId1"
+ private val appId2 = "appId2"
+
+ private val apps = mapOf(appId1 to "$appId1-name", appId2 to "$appId2-name")
+ private val outMessages = mutableListOf>()
+ private val inMessages = mutableListOf>()
+ private var messenger = GarminMessenger(
+ aapsLogger, context, apps, { app, msg -> inMessages.add(app to msg) },
+ enableConnectIq = false, enableSimulator = false)
+ private val client1 = mock() {
+ on { name } doReturn "Mock1"
+ on { sendMessage(any(), any()) } doAnswer { a ->
+ outMessages.add(a.getArgument(0) to a.getArgument(1))
+ Unit
+ }
+ }
+ private val client2 = mock() {
+ on { name } doReturn "Mock2"
+ on { sendMessage(any(), any()) } doAnswer { a ->
+ outMessages.add(a.getArgument(0) to a.getArgument(1))
+ Unit
+ }
+ }
+ private val device1 = GarminDevice(client1, 11L, "dev1-name")
+ private val device2 = GarminDevice(client2, 12L, "dev2-name")
+
+ @BeforeEach
+ fun setup() {
+ messenger.onConnect(client1)
+ messenger.onConnect(client2)
+ client1.stub {
+ on { connectedDevices } doReturn listOf(device1)
+ }
+ client2.stub {
+ on { connectedDevices } doReturn listOf(device2)
+ }
+ }
+ @AfterEach
+ fun cleanup() {
+ messenger.dispose()
+ verify(client1).dispose()
+ verify(client2).dispose()
+ assertTrue(messenger.isDisposed)
+ }
+
+ @Test
+ fun onDisconnect() {
+ messenger.onDisconnect(client1)
+ val msg = "foo"
+ messenger.sendMessage(msg)
+ outMessages.forEach { (app, payload) ->
+ assertEquals(client2, app.device.client)
+ assertEquals(msg, GarminSerializer.deserialize(payload))
+ }
+ }
+
+ @Test
+ fun onReceiveMessage() {
+ val data = GarminSerializer.serialize("foo")
+ messenger.onReceiveMessage(client1, device1.id, appId1, data)
+ val (app, payload) = inMessages.removeAt(0)
+ assertEquals(appId1, app.id)
+ assertEquals("foo", payload)
+ }
+
+ @Test
+ fun sendMessageDevice() {
+ messenger.sendMessage(device1, "foo")
+ assertEquals(2, outMessages.size)
+ val msg1 = outMessages.first { (app, _) -> app.id == appId1 }.second
+ val msg2 = outMessages.first { (app, _) -> app.id == appId2 }.second
+ assertEquals("foo", GarminSerializer.deserialize(msg1))
+ assertEquals("foo", GarminSerializer.deserialize(msg2))
+ messenger.onSendMessage(client1, device1.id, appId1, null)
+ }
+
+ @Test
+ fun onSendMessageAll() {
+ messenger.sendMessage(listOf("foo"))
+ assertEquals(4, outMessages.size)
+ val msg11 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId1 }.second
+ val msg12 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId2 }.second
+ val msg21 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId1 }.second
+ val msg22 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId2 }.second
+ assertEquals(listOf("foo"), GarminSerializer.deserialize(msg11))
+ assertEquals(listOf("foo"), GarminSerializer.deserialize(msg12))
+ assertEquals(listOf("foo"), GarminSerializer.deserialize(msg21))
+ assertEquals(listOf("foo"), GarminSerializer.deserialize(msg22))
+ messenger.onSendMessage(client1, device1.id, appId1, null)
+ }
+}
\ No newline at end of file
diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt
index 18c772877e9..e1e9fa89a94 100644
--- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt
+++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt
@@ -1,5 +1,6 @@
package app.aaps.plugins.sync.garmin
+import android.content.Context
import app.aaps.core.interfaces.db.GlucoseUnit
import app.aaps.core.interfaces.resources.ResourceHelper
import app.aaps.core.interfaces.rx.events.EventNewBG
@@ -11,9 +12,14 @@ import dagger.android.HasAndroidInjector
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito.atMost
import org.mockito.Mockito.mock
@@ -21,19 +27,28 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+import java.net.ConnectException
+import java.net.HttpURLConnection
import java.net.SocketAddress
import java.net.URI
import java.time.Clock
+import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.time.temporal.ChronoUnit
import java.util.concurrent.locks.Condition
+import kotlin.ranges.LongProgression.Companion.fromClosedRange
class GarminPluginTest: TestBase() {
private lateinit var gp: GarminPlugin
@Mock private lateinit var rh: ResourceHelper
@Mock private lateinit var sp: SP
+ @Mock private lateinit var context: Context
@Mock private lateinit var loopHub: LoopHub
private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC"))
@@ -44,14 +59,23 @@ class GarminPluginTest: TestBase() {
@BeforeEach
fun setup() {
- gp = GarminPlugin(injector, aapsLogger, rh, loopHub, rxBus, sp)
+ gp = GarminPlugin(injector, aapsLogger, rh, context, loopHub, rxBus, sp)
gp.clock = clock
`when`(loopHub.currentProfileName).thenReturn("Default")
+ `when`(sp.getBoolean(anyString(), anyBoolean())).thenAnswer { i -> i.arguments[1] }
+ `when`(sp.getString(anyString(), anyString())).thenAnswer { i -> i.arguments[1] }
+ `when`(sp.getInt(anyString(), anyInt())).thenAnswer { i -> i.arguments[1] }
+ `when`(sp.getInt(eq("communication_http_port"), anyInt()))
+ .thenReturn(28890)
}
@AfterEach
fun verifyNoFurtherInteractions() {
verify(loopHub, atMost(2)).currentProfileName
+ verify(loopHub, atMost(3)).insulinOnboard
+ verify(loopHub, atMost(3)).insulinBasalOnboard
+ verify(loopHub, atMost(3)).temporaryBasal
+ verify(loopHub, atMost(3)).carbsOnboard
verifyNoMoreInteractions(loopHub)
}
@@ -71,11 +95,23 @@ class GarminPluginTest: TestBase() {
"device" to "Test_Device")
private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue(
+ id = 10 * timestamp.toEpochMilli(),
timestamp = timestamp.toEpochMilli(), raw = 90.0, value = value,
- trendArrow = GlucoseValue.TrendArrow.FLAT, noise = null,
+ trendArrow = GlucoseValue.TrendArrow.FLAT, noise = 4.5,
sourceSensor = GlucoseValue.SourceSensor.RANDOM
)
+ @Test
+ fun testReceiveHeartRateMap() {
+ val hr = createHeartRate(80)
+ gp.receiveHeartRate(hr, false)
+ verify(loopHub).storeHeartRate(
+ Instant.ofEpochSecond(hr["hrStart"] as Long),
+ Instant.ofEpochSecond(hr["hrEnd"] as Long),
+ 80,
+ hr["device"] as String)
+ }
+
@Test
fun testReceiveHeartRateUri() {
val hr = createHeartRate(99)
@@ -119,6 +155,132 @@ class GarminPluginTest: TestBase() {
verify(loopHub).getGlucoseValues(from, true)
}
+ @Test
+ fun setupHttpServer_enabled() {
+ `when`(sp.getBoolean("communication_http", false)).thenReturn(true)
+ `when`(sp.getInt("communication_http_port", 28891)).thenReturn(28892)
+ gp.setupHttpServer(Duration.ofSeconds(10))
+ val reqUri = URI("http://127.0.0.1:28892/get")
+ val resp = reqUri.toURL().openConnection() as HttpURLConnection
+ assertEquals(200, resp.responseCode)
+
+ // Change port
+ `when`(sp.getInt("communication_http_port", 28891)).thenReturn(28893)
+ gp.setupHttpServer(Duration.ofSeconds(10))
+ val reqUri2 = URI("http://127.0.0.1:28893/get")
+ val resp2 = reqUri2.toURL().openConnection() as HttpURLConnection
+ assertEquals(200, resp2.responseCode)
+
+ `when`(sp.getBoolean("communication_http", false)).thenReturn(false)
+ gp.setupHttpServer(Duration.ofSeconds(10))
+ assertThrows(ConnectException::class.java) {
+ (reqUri2.toURL().openConnection() as HttpURLConnection).responseCode
+ }
+ gp.onStop()
+
+ verify(loopHub, times(2)).getGlucoseValues(anyObject(), eq(true))
+ verify(loopHub, times(2)).insulinOnboard
+ verify(loopHub, times(2)).temporaryBasal
+ verify(loopHub, times(2)).isConnected
+ verify(loopHub, times(2)).glucoseUnit
+ }
+
+ @Test
+ fun setupHttpServer_disabled() {
+ gp.setupHttpServer(Duration.ofSeconds(10))
+ val reqUri = URI("http://127.0.0.1:28890/get")
+ assertThrows(ConnectException::class.java) {
+ (reqUri.toURL().openConnection() as HttpURLConnection).responseCode
+ }
+ }
+
+ @Test
+ fun requestHandler_NoKey() {
+ val uri = createUri(emptyMap())
+ val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" }
+ assertEquals(
+ HttpURLConnection.HTTP_OK to "OK",
+ handler(mock(SocketAddress::class.java), uri, null))
+ }
+
+ @Test
+ fun requestHandler_KeyProvided() {
+ val uri = createUri(mapOf("key" to "foo"))
+ val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" }
+ assertEquals(
+ HttpURLConnection.HTTP_OK to "OK",
+ handler(mock(SocketAddress::class.java), uri, null))
+ }
+
+ @Test
+ fun requestHandler_KeyRequiredAndProvided() {
+ `when`(sp.getString("garmin_aaps_key", "")).thenReturn("foo")
+ val uri = createUri(mapOf("key" to "foo"))
+ val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" }
+ assertEquals(
+ HttpURLConnection.HTTP_OK to "OK",
+ handler(mock(SocketAddress::class.java), uri, null))
+
+ }
+
+ @Test
+ fun requestHandler_KeyRequired() {
+ gp.garminMessenger = mock(GarminMessenger::class.java)
+
+ `when`(sp.getString("garmin_aaps_key", "")).thenReturn("foo")
+ val uri = createUri(emptyMap())
+ val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" }
+ assertEquals(
+ HttpURLConnection.HTTP_UNAUTHORIZED to "{}",
+ handler(mock(SocketAddress::class.java), uri, null))
+
+ val captor = ArgumentCaptor.forClass(Any::class.java)
+ verify(gp.garminMessenger)!!.sendMessage(captor.capture() ?: "")
+ @Suppress("UNCHECKED_CAST")
+ val r = captor.value as Map
+ assertEquals("foo", r["key"])
+ assertEquals("glucose", r["command"])
+ assertEquals("D", r["profile"])
+ assertEquals("", r["encodedGlucose"])
+ assertEquals(0.0, r["remainingInsulin"])
+ assertEquals("mmoll", r["glucoseUnit"])
+ assertEquals(0.0, r["temporaryBasalRate"])
+ assertEquals(false, r["connected"])
+ assertEquals(clock.instant().epochSecond, r["timestamp"])
+ verify(loopHub).getGlucoseValues(getGlucoseValuesFrom, true)
+ verify(loopHub).insulinOnboard
+ verify(loopHub).temporaryBasal
+ verify(loopHub).isConnected
+ verify(loopHub).glucoseUnit
+ }
+
+ @Test
+ fun onConnectDevice() {
+ gp.garminMessenger = mock(GarminMessenger::class.java)
+ `when`(sp.getString("garmin_aaps_key", "")).thenReturn("foo")
+ val device = GarminDevice(mock(),1, "Edge")
+ gp.onConnectDevice(device)
+
+ val captor = ArgumentCaptor.forClass(Any::class.java)
+ verify(gp.garminMessenger)!!.sendMessage(eq(device), captor.capture() ?: "")
+ @Suppress("UNCHECKED_CAST")
+ val r = captor.value as Map
+ assertEquals("foo", r["key"])
+ assertEquals("glucose", r["command"])
+ assertEquals("D", r["profile"])
+ assertEquals("", r["encodedGlucose"])
+ assertEquals(0.0, r["remainingInsulin"])
+ assertEquals("mmoll", r["glucoseUnit"])
+ assertEquals(0.0, r["temporaryBasalRate"])
+ assertEquals(false, r["connected"])
+ assertEquals(clock.instant().epochSecond, r["timestamp"])
+ verify(loopHub).getGlucoseValues(getGlucoseValuesFrom, true)
+ verify(loopHub).insulinOnboard
+ verify(loopHub).temporaryBasal
+ verify(loopHub).isConnected
+ verify(loopHub).glucoseUnit
+ }
+
@Test
fun testOnGetBloodGlucose() {
`when`(loopHub.isConnected).thenReturn(true)
@@ -129,7 +291,7 @@ class GarminPluginTest: TestBase() {
listOf(createGlucoseValue(Instant.ofEpochSecond(1_000))))
val hr = createHeartRate(99)
val uri = createUri(hr)
- val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null)
+ val result = gp.onGetBloodGlucose(uri)
assertEquals(
"{\"encodedGlucose\":\"0A+6AQ==\"," +
"\"remainingInsulin\":3.14," +
@@ -161,7 +323,7 @@ class GarminPluginTest: TestBase() {
params["wait"] = 10
val uri = createUri(params)
gp.newValue = mock(Condition::class.java)
- val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null)
+ val result = gp.onGetBloodGlucose(uri)
assertEquals(
"{\"encodedGlucose\":\"/wS6AQ==\"," +
"\"remainingInsulin\":3.14," +
@@ -184,7 +346,7 @@ class GarminPluginTest: TestBase() {
@Test
fun testOnPostCarbs() {
val uri = createUri(mapOf("carbs" to "12"))
- assertEquals("", gp.onPostCarbs(mock(SocketAddress::class.java), uri, null))
+ assertEquals("", gp.onPostCarbs(uri))
verify(loopHub).postCarbs(12)
}
@@ -192,7 +354,7 @@ class GarminPluginTest: TestBase() {
fun testOnConnectPump_Disconnect() {
val uri = createUri(mapOf("disconnectMinutes" to "20"))
`when`(loopHub.isConnected).thenReturn(false)
- assertEquals("{\"connected\":false}", gp.onConnectPump(mock(SocketAddress::class.java), uri, null))
+ assertEquals("{\"connected\":false}", gp.onConnectPump(uri))
verify(loopHub).disconnectPump(20)
verify(loopHub).isConnected
}
@@ -201,8 +363,69 @@ class GarminPluginTest: TestBase() {
fun testOnConnectPump_Connect() {
val uri = createUri(mapOf("disconnectMinutes" to "0"))
`when`(loopHub.isConnected).thenReturn(true)
- assertEquals("{\"connected\":true}", gp.onConnectPump(mock(SocketAddress::class.java), uri, null))
+ assertEquals("{\"connected\":true}", gp.onConnectPump(uri))
verify(loopHub).connectPump()
verify(loopHub).isConnected
}
+
+ @Test
+ fun onSgv_NoGlucose() {
+ whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
+ whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn(emptyList())
+ assertEquals("[]", gp.onSgv(createUri(mapOf())))
+ verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false)
+ }
+
+ @Test
+fun onSgv_NoDelta() {
+ whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
+ whenever(loopHub.insulinOnboard).thenReturn(2.7)
+ whenever(loopHub.insulinBasalOnboard).thenReturn(2.5)
+ whenever(loopHub.temporaryBasal).thenReturn(0.8)
+ whenever(loopHub.carbsOnboard).thenReturn(10.7)
+ whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn(
+ listOf(createGlucoseValue(
+ clock.instant().minusSeconds(100L), 99.3)))
+ assertEquals(
+ """[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7}]""",
+ gp.onSgv(createUri(mapOf())))
+ verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false)
+ verify(loopHub).glucoseUnit
+ }
+
+ @Test
+ fun onSgv() {
+ whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
+ whenever(loopHub.insulinOnboard).thenReturn(2.7)
+ whenever(loopHub.insulinBasalOnboard).thenReturn(2.5)
+ whenever(loopHub.temporaryBasal).thenReturn(0.8)
+ whenever(loopHub.carbsOnboard).thenReturn(10.7)
+ whenever(loopHub.getGlucoseValues(any(), eq(false))).thenAnswer { i ->
+ val from = i.getArgument(0)
+ fromClosedRange(from.toEpochMilli(), clock.instant().toEpochMilli(), 300_000L)
+ .map(Instant::ofEpochMilli)
+ .mapIndexed { idx, ts -> createGlucoseValue(ts, 100.0+(10 * idx)) }.reversed()}
+ assertEquals(
+ """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7}]""",
+ gp.onSgv(createUri(mapOf("count" to "1"))))
+ verify(loopHub).getGlucoseValues(
+ clock.instant().minusSeconds(600L), false)
+
+
+ assertEquals(
+ """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7},""" +
+ """{"_id":"-2900000","device":"RANDOM","deviceString":"1969-12-31T23:55:10Z","sysTime":"1969-12-31T23:55:10Z","unfiltered":90.0,"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""",
+ gp.onSgv(createUri(mapOf("count" to "2"))))
+ verify(loopHub).getGlucoseValues(
+ clock.instant().minusSeconds(900L), false)
+
+ assertEquals(
+ """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7},""" +
+ """{"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""",
+ gp.onSgv(createUri(mapOf("count" to "2", "brief_mode" to "true"))))
+ verify(loopHub, times(2)).getGlucoseValues(
+ clock.instant().minusSeconds(900L), false)
+
+ verify(loopHub, atLeastOnce()).glucoseUnit
+ }
}
diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSerializerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSerializerTest.kt
new file mode 100644
index 00000000000..fed7e32a286
--- /dev/null
+++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSerializerTest.kt
@@ -0,0 +1,92 @@
+package app.aaps.plugins.sync.garmin
+
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import kotlin.test.assertContentEquals
+
+class GarminSerializerTest {
+
+ @Test fun testSerializeDeserializeString() {
+ val o = "Hello, world!"
+ val data = GarminSerializer.serialize(o)
+ assertContentEquals(
+ byteArrayOf(
+ -85, -51, -85, -51, 0, 0, 0, 16, 0, 14, 72, 101, 108, 108, 111, 44, 32, 119, 111,
+ 114, 108, 100, 33, 0, -38, 122, -38, 122, 0, 0, 0, 5, 3, 0,0, 0, 0),
+ data)
+ assertEquals(o, GarminSerializer.deserialize(data))
+ }
+
+ @Test fun testSerializeDeserializeInteger() {
+ val o = 3
+ val data = GarminSerializer.serialize(o)
+ assertContentEquals(
+ byteArrayOf(-38, 122, -38, 122, 0, 0, 0, 5, 1, 0, 0, 0, 3),
+ data)
+ assertEquals(o, GarminSerializer.deserialize(data))
+ }
+
+ @Test fun tesSerializeDeserializeArray() {
+ val o = listOf("a", "b", true, 3, 3.4F, listOf(5L, 9), 42)
+ val data = GarminSerializer.serialize(o)
+ assertContentEquals(
+ byteArrayOf(
+ -85, -51, -85, -51, 0, 0, 0, 8, 0, 2, 97, 0, 0, 2, 98, 0, -38, 122, -38, 122, 0, 0,
+ 0, 55, 5, 0, 0, 0, 7, 3, 0, 0, 0, 0, 3, 0, 0, 0, 4, 9, 1, 1, 0, 0, 0, 3, 2, 64, 89,
+ -103, -102, 5, 0, 0, 0, 2, 1, 0, 0, 0, 42, 14, 0, 0, 0, 0, 0, 0, 0, 5, 14, 0, 0, 0,
+ 0, 0, 0, 0, 9),
+ data)
+ assertEquals(o, GarminSerializer.deserialize(data))
+ }
+
+ @Test
+ fun testSerializeDeserializeMap() {
+ val o = mapOf("a" to "abc", "c" to 3, "d" to listOf(4, 9, "abc"), true to null)
+ val data = GarminSerializer.serialize(o)
+ assertContentEquals(
+ byteArrayOf(
+ -85, -51, -85, -51, 0, 0, 0, 18, 0, 2, 97, 0, 0, 4, 97, 98, 99, 0, 0, 2, 99, 0, 0,
+ 2, 100, 0, -38, 122, -38, 122, 0, 0, 0, 53, 11, 0, 0, 0, 4, 3, 0, 0, 0, 0, 3, 0, 0,
+ 0, 4, 3, 0, 0, 0, 10, 1, 0, 0, 0, 3, 3, 0, 0, 0, 14, 5, 0, 0, 0, 3, 9, 1, 0, 1, 0, 0,
+ 0, 4, 1, 0, 0, 0, 9, 3, 0, 0, 0, 4),
+ data)
+ assertEquals(o, GarminSerializer.deserialize(data))
+ }
+
+ @Test fun testSerializeDeserializeNull() {
+ val o = null
+ val data = GarminSerializer.serialize(o)
+ assertContentEquals(
+ byteArrayOf(-38, 122, -38, 122, 0, 0, 0, 1, 0),
+ data)
+ assertEquals(o, GarminSerializer.deserialize(data))
+ assertEquals(o, GarminSerializer.deserialize(data))
+ }
+
+ @Test fun testSerializeDeserializeAllPrimitiveTypes() {
+ val o = listOf(1, 1.2F, 1.3, "A", true, 2L, 'X', null)
+ val data = GarminSerializer.serialize(o)
+ assertContentEquals(
+ byteArrayOf(
+ -85, -51, -85, -51, 0, 0, 0, 4, 0, 2, 65, 0, -38, 122, -38, 122, 0, 0, 0, 46, 5, 0,
+ 0, 0, 8, 1, 0, 0, 0, 1, 2, 63, -103, -103, -102, 15, 63, -12, -52, -52, -52, -52,
+ -52, -51, 3, 0, 0, 0, 0, 9, 1, 14, 0, 0, 0, 0, 0, 0, 0, 2, 19, 0, 0, 0, 88, 0),
+ data)
+ assertEquals(o, GarminSerializer.deserialize(data))
+ assertEquals(o, GarminSerializer.deserialize(data))
+ }
+
+ @Test fun testSerializeDeserializeMapNested() {
+ val o = mapOf("a" to "abc", "c" to 3, "d" to listOf(4, 9, "abc"))
+ val data = GarminSerializer.serialize(o)
+ assertContentEquals(
+ byteArrayOf(
+ -85, -51, -85, -51, 0, 0, 0, 18, 0, 2, 97, 0, 0, 4, 97, 98, 99, 0, 0, 2, 99, 0, 0,
+ 2, 100, 0, -38, 122, -38, 122, 0, 0, 0, 50, 11, 0, 0, 0, 3, 3, 0, 0, 0, 0, 3, 0, 0,
+ 0, 4, 3, 0, 0, 0, 10, 1, 0, 0, 0, 3, 3, 0, 0, 0, 14, 5, 0, 0, 0, 3, 1, 0, 0, 0, 4,
+ 1, 0, 0, 0, 9, 3, 0, 0, 0, 4),
+ data)
+ assertEquals(o, GarminSerializer.deserialize(data))
+ }
+}
\ No newline at end of file
diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt
new file mode 100644
index 00000000000..9243260d07c
--- /dev/null
+++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt
@@ -0,0 +1,68 @@
+package app.aaps.plugins.sync.garmin
+
+import app.aaps.shared.tests.TestBase
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.timeout
+import org.mockito.kotlin.verify
+import java.net.Inet4Address
+import java.net.Socket
+import java.time.Duration
+
+class GarminSimulatorClientTest: TestBase() {
+
+ private lateinit var client: GarminSimulatorClient
+ private val receiver: GarminReceiver = mock()
+
+ private fun waitForOrFail(c: ()->T?): T {
+ for (i in 0 until 10) {
+ c()?.let { return it }
+ Thread.sleep(1)
+ }
+ throw AssertionError("wait timed out")
+ }
+
+ @BeforeEach
+ fun setup() {
+ client = GarminSimulatorClient(aapsLogger, receiver, 0)
+ }
+
+ @Test
+ fun receiveMessage() {
+ val payload = "foo".toByteArray()
+ assertTrue(client.awaitReady(Duration.ofSeconds(10)))
+ verify(receiver, timeout(100)).onConnect(client)
+ val port = client.port
+ val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1))
+ Socket(ip, port).use { socket ->
+ assertTrue(socket.isConnected)
+ socket.getOutputStream().write(payload)
+ socket.getOutputStream().flush()
+ val device = waitForOrFail { client.connectedDevices.firstOrNull() }
+ verify(receiver, timeout(1_000))
+ .onReceiveMessage(eq(client), eq(device.id), eq("SIMAPP"), eq(payload))
+ }
+ }
+
+ @Test
+ fun sendMessage() {
+ val payload = "foo".toByteArray()
+ assertTrue(client.awaitReady(Duration.ofSeconds(10)))
+ verify(receiver, timeout(100)).onConnect(client)
+ val port = client.port
+ val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1))
+ val device: GarminDevice
+ val app: GarminApplication
+ Socket(ip, port).use { socket ->
+ assertTrue(socket.isConnected)
+ device = waitForOrFail { client.connectedDevices.firstOrNull() }
+ app = GarminApplication(device, "SIMAPP", "T")
+ client.sendMessage(app, payload)
+ }
+ verify(receiver, timeout(1_000)).onSendMessage(eq(client), eq(device.id), eq(app.id), isNull())
+ }
+}
\ No newline at end of file
diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt
index d89dfa91563..cfaa34b1d6e 100644
--- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt
+++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt
@@ -77,7 +77,7 @@ internal class HttpServerTest: TestBase() {
HttpServer(aapsLogger, port).use { server ->
server.registerEndpoint("/foo") { _: SocketAddress, uri: URI, _: String? ->
assertEquals(URI("/foo"), uri)
- "test"
+ HttpURLConnection.HTTP_OK to "test"
}
assertTrue(server.awaitReady(Duration.ofSeconds(10)))
val resp = reqUri.toURL().openConnection() as HttpURLConnection
diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt
index a88facfa056..c96bcaee4bb 100644
--- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt
+++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt
@@ -5,6 +5,7 @@ import app.aaps.core.interfaces.aps.Loop
import app.aaps.core.interfaces.constraints.Constraint
import app.aaps.core.interfaces.constraints.ConstraintsChecker
import app.aaps.core.interfaces.db.GlucoseUnit
+import app.aaps.core.interfaces.iob.CobInfo
import app.aaps.core.interfaces.iob.IobCobCalculator
import app.aaps.core.interfaces.iob.IobTotal
import app.aaps.core.interfaces.logging.UserEntryLogger
@@ -13,6 +14,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction
import app.aaps.core.interfaces.pump.DetailedBolusInfo
import app.aaps.core.interfaces.queue.CommandQueue
import app.aaps.core.interfaces.sharedPreferences.SP
+import app.aaps.core.main.graph.OverviewData
import app.aaps.database.ValueWrapper
import app.aaps.database.entities.EffectiveProfileSwitch
import app.aaps.database.entities.GlucoseValue
@@ -54,6 +56,7 @@ class LoopHubTest: TestBase() {
@Mock lateinit var repo: AppRepository
@Mock lateinit var userEntryLogger: UserEntryLogger
@Mock lateinit var sp: SP
+ @Mock lateinit var overviewData: OverviewData
private lateinit var loopHub: LoopHubImpl
private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC"))
@@ -62,7 +65,7 @@ class LoopHubTest: TestBase() {
fun setup() {
loopHub = LoopHubImpl(
aapsLogger, commandQueue, constraints, iobCobCalculator, loop,
- profileFunction, repo, userEntryLogger, sp
+ profileFunction, repo, userEntryLogger, sp, overviewData
)
loopHub.clock = clock
}
@@ -76,9 +79,10 @@ class LoopHubTest: TestBase() {
verifyNoMoreInteractions(profileFunction)
verifyNoMoreInteractions(repo)
verifyNoMoreInteractions(userEntryLogger)
+ verifyNoMoreInteractions(overviewData)
}
- @Test
+@Test
fun testCurrentProfile() {
val profile = mock(Profile::class.java)
`when`(profileFunction.getProfile()).thenReturn(profile)
@@ -109,6 +113,22 @@ class LoopHubTest: TestBase() {
verify(iobCobCalculator, times(1)).calculateIobFromBolus()
}
+ @Test
+ fun testBasalOnBoard() {
+ val iobBasal = IobTotal(time = 0).apply { basaliob = 23.9 }
+ `when`(iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended()).thenReturn(iobBasal)
+ assertEquals(23.9, loopHub.insulinBasalOnboard, 1e-10)
+ verify(iobCobCalculator, times(1)).calculateIobFromTempBasalsIncludingConvertedExtended()
+ }
+
+ @Test
+ fun testCarbsOnBoard() {
+ val cobInfo = CobInfo(0, 12.0, 0.0)
+ `when`(overviewData.cobInfo(iobCobCalculator)).thenReturn(cobInfo)
+ assertEquals(12.0, loopHub.carbsOnboard)
+ verify(overviewData, times(1)).cobInfo(iobCobCalculator)
+ }
+
@Test
fun testIsConnected() {
`when`(loop.isDisconnected).thenReturn(false)
@@ -247,4 +267,4 @@ class LoopHubTest: TestBase() {
samplingStart, samplingEnd, 101, "Test Device")
verify(repo).runTransaction(InsertOrUpdateHeartRateTransaction(hr))
}
-}
\ No newline at end of file
+}
diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt
similarity index 97%
rename from plugins/sync/src/test/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPluginTest.kt
rename to plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt
index 650eaa7bb50..fc42eba3079 100644
--- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPluginTest.kt
+++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt
@@ -1,4 +1,4 @@
-package app.aaps.plugins.sync.dataBroadcaster
+package app.aaps.plugins.sync.tizen
import app.aaps.core.interfaces.aps.AutosensDataStore
import app.aaps.core.interfaces.aps.Loop
@@ -27,7 +27,7 @@ import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock
import org.mockito.Mockito
-internal class DataBroadcastPluginTest : TestBaseWithProfile() {
+internal class TizenPluginTest : TestBaseWithProfile() {
@Mock lateinit var defaultValueHelper: DefaultValueHelper
@Mock lateinit var loop: Loop
@@ -36,13 +36,13 @@ internal class DataBroadcastPluginTest : TestBaseWithProfile() {
@Mock lateinit var autosensDataStore: AutosensDataStore
@Mock lateinit var processedDeviceStatusData: ProcessedDeviceStatusData
- private lateinit var sut: DataBroadcastPlugin
+ private lateinit var sut: TizenPlugin
private val injector = HasAndroidInjector { AndroidInjector { } }
@BeforeEach
fun setUp() {
- sut = DataBroadcastPlugin(
+ sut = TizenPlugin(
injector, aapsLogger, rh, aapsSchedulers, context, dateUtil, fabricPrivacy, rxBus, iobCobCalculator, profileFunction, defaultValueHelper, processedDeviceStatusData,
loop, activePlugin, receiverStatusStore, config, glucoseStatusProvider, decimalFormatter
)
diff --git a/pump/combo/src/main/res/values-ru-rRU/strings.xml b/pump/combo/src/main/res/values-ru-rRU/strings.xml
index a0e9709c2a3..77f6a643c0c 100644
--- a/pump/combo/src/main/res/values-ru-rRU/strings.xml
+++ b/pump/combo/src/main/res/values-ru-rRU/strings.xml
@@ -28,23 +28,23 @@
Нормальный
Необходимо обновить часы помпы
Предупреждение об отмене скорости временного базала подтверждено
- Не удалось подключиться к помпе. Болюс не подан
- Подача болюса не состоялась. Чтобы удостовериться, проверьте помпу во избежание двойного болюса и повторите подачу. Для защиты от ложных срабатываний болюсы не повторяются автоматически.
- Подано только %1$.2f ед. из запрошенного болюса %2$.2f ед. из-за ошибки. Пожалуйста, проверьте помпу, чтобы удостовериться в этом и принять соответствующие меры.
- Подача болюса и проверка истории помпы не состоялись, пожалуйста проверьте помпу. Если болюс был подан, он будет добавлен в назначения во время следующего соединения с помпой.
+ Не удалось подключиться к помпе. Болюс не введен
+ Болюс не введен. Чтобы удостовериться, проверьте помпу во избежание двойного болюса и повторите подачу. Для защиты от ложных срабатываний болюсы не повторяются автоматически.
+ Введено только %1$.2f ед. из запрошенного болюса %2$.2f ед. из-за ошибки. Пожалуйста, проверьте помпу, чтобы удостовериться в этом и принять соответствующие меры.
+ Ввод болюса и проверка истории помпы не состоялись, пожалуйста проверьте помпу. Если болюс был введен, он будет добавлен в назначения во время следующего соединения с помпой.
В резервуаре недостаточно инсулина для болюса
Недопустимые установки помпы, проверьте документацию и убедитесь, что меню Quick Info называется QUICK INFO, используя приложение 360 для конфигурации помпы.
Чтение базального профиля
- История событий помпы изменилась с момента вычисления болюса. Болюс не подан. Пожалуйста пересчитайте потребность в болюсе.
- Болюс подан успешно, но запись о назначении не сделана. Это может быть вызвано тем, что за последние две минуты назначено два болюса одного объема. Пожалуйста проверьте историю событий помпы и записи о назначениях и добавьте пропущенные записи из Портала назначений. Не добавляйте записи с одним и тем же временем и одинаковым объемом.
+ История событий помпы изменилась с момента вычисления болюса. Болюс не введен. Пожалуйста пересчитайте потребность в болюсе.
+ Болюс введен успешно, но запись о назначении не сделана. Это может быть вызвано тем, что за последние две минуты назначено два болюса одного объема. Пожалуйста проверьте историю событий помпы и записи о назначениях и добавьте пропущенные записи из Портала назначений. Не добавляйте записи с одним и тем же временем и одинаковым объемом.
Временная верхняя цель отклонена т. к. калькуляция не учитывала недавние изменения в истории событий помпы
Обновление статуса помпы
Скорость базала на помпе изменилась и вскоре будет обновлена
Скорость базала на помпе изменилась, но учесть ее не удалось
Проверка изменений в истории событий помпы
Только что импортировано несколько болюсов с одинаковым количеством инсулина в течение одной минуты. В лог лечения может быть добавлена только одна запись. Пожалуйста проверьте помпу и вручную введите запись о болюсе через вкладку портала назначений. Убедитесь, что данному времени соответствует только одна запись о болюсе.
- Новый болюс старше 24 часов или запись относится к будущему. Пожалуйста убедитесь что дата на помпе установлена правильно.
- Время/дата поданного болюса неверны. Вероятно, кол-во активного инсулина IOB также неверно. Проверьте время/дату помпы.
+ Новый болюс старше 24 часов или запись относится к будущему. Убедитесь что дата на помпе установлена правильно.
+ Время/дата болюса неверны. Вероятно, кол-во активного инсулина IOB также неверно. Проверьте время/дату помпы.
Отсчет болюса
Отсчет временного базала TBR
Болюс остановлен
diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt
index 7ea2c36e19d..606d1058e39 100644
--- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt
+++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt
@@ -153,7 +153,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.ClearAlarm,
PumpHistoryEntryType.ChangeAlarmNotifyMode,
PumpHistoryEntryType.EnableDisableRemote,
- PumpHistoryEntryType.BGReceived,
PumpHistoryEntryType.SensorAlert,
PumpHistoryEntryType.ChangeTimeFormat,
PumpHistoryEntryType.ChangeReservoirWarningTime,
@@ -188,7 +187,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.ChangeWatchdogEnable,
PumpHistoryEntryType.ChangeOtherDeviceID,
PumpHistoryEntryType.ReadOtherDevicesIDs,
- PumpHistoryEntryType.BGReceived512,
PumpHistoryEntryType.SensorStatus,
PumpHistoryEntryType.ReadCaptureEventEnabled,
PumpHistoryEntryType.ChangeCaptureEventEnable,
@@ -206,6 +204,12 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.UnabsorbedInsulin,
PumpHistoryEntryType.UnabsorbedInsulin512 -> RecordDecodeStatus.Ignored
+ PumpHistoryEntryType.BGReceived,
+ PumpHistoryEntryType.BGReceived512 -> {
+ decodeBgReceived(entry)
+ RecordDecodeStatus.OK
+ }
+
PumpHistoryEntryType.DailyTotals522,
PumpHistoryEntryType.DailyTotals523,
PumpHistoryEntryType.DailyTotals515,
@@ -297,7 +301,9 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
}
private fun decodeBatteryActivity(entry: PumpHistoryEntry) {
- entry.displayableValue = if (entry.head[0] == 0.toByte()) "Battery Removed" else "Battery Replaced"
+ val isRemoved = entry.head[0] == 0.toByte()
+ entry.addDecodedData("isRemoved", isRemoved)
+ entry.displayableValue = if (isRemoved) "Battery Removed" else "Battery Replaced"
}
private fun decodeBasalProfileStart(entry: PumpHistoryEntry): RecordDecodeStatus {
@@ -407,8 +413,11 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
}
private fun decodeBgReceived(entry: PumpHistoryEntry) {
- entry.addDecodedData("amount", (ByteUtil.asUINT8(entry.getRawDataByIndex(0)) shl 3) + (ByteUtil.asUINT8(entry.getRawDataByIndex(3)) shr 5))
- entry.addDecodedData("meter", ByteUtil.substring(entry.rawData, 6, 3)) // index moved from 1 -> 0
+ val glucoseMgdl = (ByteUtil.asUINT8(entry.head[0]) shl 3) + (ByteUtil.asUINT8(entry.datetime[2]) shr 5)
+ val meterSerial = ByteUtil.shortHexStringWithoutSpaces(entry.body)
+ entry.addDecodedData("GlucoseMgdl", glucoseMgdl)
+ entry.addDecodedData("MeterSerial", meterSerial)
+ entry.displayableValue = String.format("Glucose: %d mg/dl, Meter Serial: %s", glucoseMgdl, meterSerial)
}
private fun decodeCalBGForPH(entry: PumpHistoryEntry) {
diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt
index 27d052bcca3..03bf6c49c3f 100644
--- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt
+++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt
@@ -4,6 +4,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.notifications.Notification
import app.aaps.core.interfaces.plugin.ActivePlugin
+import app.aaps.core.interfaces.profile.ProfileUtil
import app.aaps.core.interfaces.pump.DetailedBolusInfo
import app.aaps.core.interfaces.pump.PumpSync
import app.aaps.core.interfaces.pump.defs.PumpType
@@ -67,7 +68,8 @@ class MedtronicHistoryData @Inject constructor(
val medtronicPumpStatus: MedtronicPumpStatus,
private val pumpSync: PumpSync,
private val pumpSyncStorage: PumpSyncStorage,
- private val uiInteraction: UiInteraction
+ private val uiInteraction: UiInteraction,
+ private val profileUtil: ProfileUtil
) {
val allHistory: MutableList = mutableListOf()
@@ -322,6 +324,17 @@ class MedtronicHistoryData @Inject constructor(
* Process History Data: Boluses(Treatments), TDD, TBRs, Suspend-Resume (or other pump stops: battery, prime)
*/
fun processNewHistoryData() {
+ // Finger BG (for adding entry to careportal)
+ val bgRecords: MutableList = getFilteredItems(setOf(PumpHistoryEntryType.BGReceived, PumpHistoryEntryType.BGReceived512))
+ aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: BGReceived [count=%d, items=%s]", bgRecords.size, gson.toJson(bgRecords)))
+ if (isCollectionNotEmpty(bgRecords)) {
+ try {
+ processBgReceived(bgRecords)
+ } catch (ex: Exception) {
+ aapsLogger.error(LTag.PUMP, "ProcessHistoryData: Error processing BGReceived entries: " + ex.message, ex)
+ throw ex
+ }
+ }
// Prime (for resetting autosense)
val primeRecords: MutableList = getFilteredItems(PumpHistoryEntryType.Prime)
@@ -347,6 +360,18 @@ class MedtronicHistoryData @Inject constructor(
}
}
+ // BatteryChange
+ val batteryChangeRecords: MutableList = getFilteredItems(PumpHistoryEntryType.BatteryChange)
+ aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: BatteryChange [count=%d, items=%s]", batteryChangeRecords.size, gson.toJson(batteryChangeRecords)))
+ if (isCollectionNotEmpty(batteryChangeRecords)) {
+ try {
+ processBatteryChange(batteryChangeRecords)
+ } catch (ex: Exception) {
+ aapsLogger.error(LTag.PUMP, "ProcessHistoryData: Error processing BatteryChange entries: " + ex.message, ex)
+ throw ex
+ }
+ }
+
// TDD
val tdds: MutableList = getFilteredItems(setOf(PumpHistoryEntryType.EndResultTotals, getTDDType()))
aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: TDD [count=%d, items=%s]", tdds.size, gson.toJson(tdds)))
@@ -407,6 +432,34 @@ class MedtronicHistoryData @Inject constructor(
}
}
+ fun processBgReceived(bgRecords: List) {
+ for (bgRecord in bgRecords) {
+ val glucoseMgdl = bgRecord.getDecodedDataEntry("GlucoseMgdl")
+ if (glucoseMgdl == null || glucoseMgdl as Int == 0) {
+ continue
+ }
+
+ val glucose = profileUtil.fromMgdlToUnits(glucoseMgdl.toDouble())
+ val glucoseUnit = profileUtil.units
+
+ val result = pumpSync.insertFingerBgIfNewWithTimestamp(
+ DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime),
+ glucose, glucoseUnit, null,
+ bgRecord.pumpId,
+ medtronicPumpStatus.pumpType,
+ medtronicPumpStatus.serialNumber
+ )
+
+ aapsLogger.debug(
+ LTag.PUMP, String.format(
+ Locale.ROOT, "insertFingerBgIfNewWithTimestamp [date=%d, glucose=%f, glucoseUnit=%s, pumpId=%d, pumpSerial=%s] - Result: %b",
+ bgRecord.atechDateTime, glucose, glucoseUnit, bgRecord.pumpId,
+ medtronicPumpStatus.serialNumber, result
+ )
+ )
+ }
+ }
+
private fun processPrime(primeRecords: List) {
val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -30)
var lastPrimeRecordTime = 0L
@@ -456,6 +509,35 @@ class MedtronicHistoryData @Inject constructor(
}
}
+ private fun processBatteryChange(batteryChangeRecords: List) {
+ val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -120)
+ var lastBatteryChangeRecordTime = 0L
+ var lastBatteryChangeRecord: PumpHistoryEntry? = null
+ for (batteryChangeRecord in batteryChangeRecords) {
+ val isRemoved = batteryChangeRecord.getDecodedDataEntry("isRemoved")
+
+ if (isRemoved != null && isRemoved as Boolean)
+ {
+ // we're interested in battery replacements, not battery removals
+ continue
+ }
+
+ if (batteryChangeRecord.atechDateTime > maxAllowedTimeInPast) {
+ if (lastBatteryChangeRecordTime < batteryChangeRecord.atechDateTime) {
+ lastBatteryChangeRecordTime = batteryChangeRecord.atechDateTime
+ lastBatteryChangeRecord = batteryChangeRecord
+ }
+ }
+ }
+ if (lastBatteryChangeRecord != null) {
+ uploadCareportalEventIfFoundInHistory(
+ lastBatteryChangeRecord,
+ MedtronicConst.Statistics.LastBatteryChange,
+ DetailedBolusInfo.EventType.PUMP_BATTERY_CHANGE
+ )
+ }
+ }
+
private fun uploadCareportalEventIfFoundInHistory(historyRecord: PumpHistoryEntry, eventSP: String, eventType: DetailedBolusInfo.EventType) {
val lastPrimeFromAAPS = sp.getLong(eventSP, 0L)
if (historyRecord.atechDateTime != lastPrimeFromAAPS) {
diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt
index 8d86bce696d..03bc600aa6d 100644
--- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt
+++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt
@@ -30,5 +30,6 @@ object MedtronicConst {
const val LastPumpHistoryEntry = StatsPrefix + "pump_history_entry"
const val LastPrime = StatsPrefix + "last_sent_prime"
const val LastRewind = StatsPrefix + "last_sent_rewind"
+ const val LastBatteryChange = StatsPrefix + "last_sent_battery_change"
}
}
\ No newline at end of file
diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt
index 03cfe9d4319..0f25f4cd989 100644
--- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt
+++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt
@@ -4,7 +4,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin
import app.aaps.core.interfaces.pump.PumpSync
import app.aaps.core.interfaces.resources.ResourceHelper
import app.aaps.core.interfaces.sharedPreferences.SP
-import app.aaps.shared.tests.TestBase
+import app.aaps.shared.tests.TestBaseWithProfile
import dagger.android.AndroidInjector
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil
@@ -16,15 +16,13 @@ import info.nightscout.pump.common.sync.PumpSyncStorage
import org.mockito.Answers
import org.mockito.Mock
-open class MedtronicTestBase : TestBase() {
+open class MedtronicTestBase : TestBaseWithProfile() {
var rileyLinkUtil = RileyLinkUtil()
@Mock lateinit var pumpSync: PumpSync
@Mock lateinit var pumpSyncStorage: PumpSyncStorage
- @Mock(answer = Answers.RETURNS_DEEP_STUBS) lateinit var activePlugin: ActivePlugin
- @Mock lateinit var sp: SP
- @Mock lateinit var rh: ResourceHelper
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS) override lateinit var activePlugin: ActivePlugin
lateinit var medtronicUtil: MedtronicUtil
lateinit var decoder: MedtronicPumpHistoryDecoder
@@ -53,6 +51,24 @@ open class MedtronicTestBase : TestBase() {
}
+ fun getPumpHistoryEntryFromData(vararg elements: Int): PumpHistoryEntry {
+ val data: MutableList = ArrayList()
+ for (item in elements) {
+ var b = if (item > 128) item - 256 else item
+ data.add(b.toByte());
+ }
+
+ val entryType = PumpHistoryEntryType.getByCode(data[0])
+
+ val phe = PumpHistoryEntry()
+ phe.setEntryType(medtronicUtil.medtronicPumpModel, entryType)
+ phe.setData(data, false)
+
+ decoder.decodeRecord(phe)
+
+ return phe
+ }
+
private fun preProcessTBRs(tbrsInput: MutableList): MutableList {
val tbrs: MutableList = mutableListOf()
val map: MutableMap = HashMap()
diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt
index 70bcd924e48..13a53c78f7b 100644
--- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt
+++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt
@@ -40,7 +40,7 @@ import org.mockito.Mock
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
medtronicHistoryData = MedtronicHistoryData(
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
- medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction
+ medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil
)
diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt
index 8b07c7dc2ca..27434397265 100644
--- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt
+++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt
@@ -1,8 +1,16 @@
package info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump
+import app.aaps.core.interfaces.ui.UiInteraction
import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase
+import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.MedtronicPumpHistoryDecoder
+import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicDeviceType
+import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus
+import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil
import com.google.common.truth.Truth.assertThat
+import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
/**
* Created by andy on 4/9/19.
@@ -10,6 +18,16 @@ import org.junit.jupiter.api.Test
*/
class PumpHistoryEntryUTest : MedtronicTestBase() {
+ @Mock lateinit var medtronicPumpStatus: MedtronicPumpStatus
+ @Mock lateinit var uiInteraction: UiInteraction
+
+ @BeforeEach
+ fun setUp() {
+ medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction)
+ `when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel)
+ decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
+ }
+
@Test
fun checkIsAfter() {
val dateObject = 20191010000000L
@@ -18,4 +36,21 @@ class PumpHistoryEntryUTest : MedtronicTestBase() {
phe.atechDateTime = dateObject
assertThat(phe.isAfter(queryObject)).isTrue()
}
+
+ @Test
+ fun decodeBgReceived() {
+ val bgRecord = getPumpHistoryEntryFromData(
+ // head
+ 0x39, 0x15,
+ // datetime (combined with glucose in mg/dl)
+ 0xC2, 0x25, 0xF3, 0x61, 0x17,
+ // serial number
+ 0x12, 0x34, 0x56
+ )
+ val expectedGlucoseMgdl = 175
+ val expectedMeterSerial = "123456"
+
+ assertThat(bgRecord.getDecodedDataEntry("GlucoseMgdl")).isEqualTo(expectedGlucoseMgdl)
+ assertThat(bgRecord.getDecodedDataEntry("MeterSerial")).isEqualTo(expectedMeterSerial)
+ }
}
diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt
index d36c40d1ba0..2a91afaba49 100644
--- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt
+++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt
@@ -1,18 +1,24 @@
package info.nightscout.androidaps.plugins.pump.medtronic.data
+import app.aaps.core.interfaces.db.GlucoseUnit
import app.aaps.core.interfaces.ui.UiInteraction
+import app.aaps.core.utils.DateTimeUtil
import com.google.gson.Gson
import com.google.gson.internal.LinkedTreeMap
import com.google.gson.reflect.TypeToken
import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase
import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.MedtronicPumpHistoryDecoder
import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntry
+import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntryType
import info.nightscout.androidaps.plugins.pump.medtronic.data.dto.TempBasalPair
+import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicDeviceType
import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus
import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import java.lang.reflect.Type
@Suppress("UNCHECKED_CAST")
@@ -24,6 +30,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
@BeforeEach
fun setUp() {
medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction)
+ `when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel)
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
}
@@ -32,7 +39,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
val unitToTest = MedtronicHistoryData(
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
- medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction
+ medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil
)
val gson = Gson()
@@ -75,7 +82,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
medtronicUtil, decoder,
medtronicPumpStatus,
pumpSync,
- pumpSyncStorage, uiInteraction
+ pumpSyncStorage, uiInteraction, profileUtil
)
val gson = Gson()
@@ -110,4 +117,70 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
}
+ @Test
+ fun processBgReceived_WithMgdl() {
+
+ val unitToTest = MedtronicHistoryData(
+ packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
+ medtronicUtil, decoder,
+ medtronicPumpStatus,
+ pumpSync,
+ pumpSyncStorage, uiInteraction, profileUtil
+ )
+
+ val glucoseMgdl = 175
+
+ `when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn(GlucoseUnit.MGDL.asText)
+
+ val bgRecord = PumpHistoryEntry()
+ bgRecord.setEntryType(medtronicUtil.medtronicPumpModel, PumpHistoryEntryType.BGReceived)
+ bgRecord.addDecodedData("GlucoseMgdl", glucoseMgdl)
+ bgRecord.addDecodedData("MeterSerial", "123456")
+
+ unitToTest.processBgReceived(listOf(bgRecord))
+
+ verify(pumpSync).insertFingerBgIfNewWithTimestamp(
+ DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime),
+ glucoseMgdl.toDouble(),
+ GlucoseUnit.MGDL, null,
+ bgRecord.pumpId,
+ medtronicPumpStatus.pumpType,
+ medtronicPumpStatus.serialNumber
+ )
+
+ }
+
+ @Test
+ fun processBgReceived_WithMmol() {
+
+ val unitToTest = MedtronicHistoryData(
+ packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
+ medtronicUtil, decoder,
+ medtronicPumpStatus,
+ pumpSync,
+ pumpSyncStorage, uiInteraction, profileUtil
+ )
+ val glucoseMgdl = 180
+ val glucoseMmol = 10.0
+
+ `when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn(GlucoseUnit.MMOL.asText)
+
+ val bgRecord = PumpHistoryEntry()
+ bgRecord.setEntryType(medtronicUtil.medtronicPumpModel, PumpHistoryEntryType.BGReceived)
+ bgRecord.addDecodedData("GlucoseMgdl", glucoseMgdl)
+ bgRecord.addDecodedData("MeterSerial", "123456")
+
+ unitToTest.processBgReceived(listOf(bgRecord))
+
+ verify(pumpSync).insertFingerBgIfNewWithTimestamp(
+ DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime),
+ glucoseMmol,
+ GlucoseUnit.MMOL, null,
+ bgRecord.pumpId,
+ medtronicPumpStatus.pumpType,
+ medtronicPumpStatus.serialNumber
+ )
+
+ }
+
}
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt
index bf15b3a2401..76abaee1f74 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt
@@ -53,6 +53,7 @@ import app.aaps.core.ui.toast.ToastUtils
import app.aaps.core.validators.ValidatingEditTextPreference
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
+import info.nightscout.pump.medtrum.comm.enums.ModelType
import info.nightscout.pump.medtrum.services.MedtrumService
import info.nightscout.pump.medtrum.ui.MedtrumOverviewFragment
import info.nightscout.pump.medtrum.util.MedtrumSnUtil
@@ -153,7 +154,7 @@ import kotlin.math.abs
override fun afterTextChanged(newValue: Editable?) {
val newSN = newValue?.toString()?.toLongOrNull(radix = 16) ?: 0
val newDeviceType = MedtrumSnUtil().getDeviceTypeFromSerial(newSN)
- editText.error = if (newDeviceType == MedtrumSnUtil.INVALID) {
+ editText.error = if (newDeviceType == ModelType.INVALID) {
rh.gs(R.string.sn_input_invalid)
} else {
null
@@ -174,7 +175,7 @@ import kotlin.math.abs
val newDeviceType = MedtrumSnUtil().getDeviceTypeFromSerial(newSN)
when {
- newDeviceType == MedtrumSnUtil.INVALID -> {
+ newDeviceType == ModelType.INVALID -> {
preferenceFragment.activity?.let { activity ->
OKDialog.show(activity, rh.gs(R.string.sn_input_title), rh.gs(R.string.sn_input_invalid))
}
@@ -183,7 +184,7 @@ import kotlin.math.abs
medtrumPump.pumpType(newDeviceType) == PumpType.MEDTRUM_UNTESTED -> {
preferenceFragment.activity?.let { activity ->
- OKDialog.show(activity, rh.gs(R.string.sn_input_title), rh.gs(R.string.pump_unsupported, newDeviceType))
+ OKDialog.show(activity, rh.gs(R.string.sn_input_title), rh.gs(R.string.pump_unsupported, newDeviceType.toString()))
}
false
}
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPump.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPump.kt
index 54dadef5055..dbea336d279 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPump.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPump.kt
@@ -18,6 +18,7 @@ import info.nightscout.pump.medtrum.comm.enums.AlarmSetting
import info.nightscout.pump.medtrum.comm.enums.AlarmState
import info.nightscout.pump.medtrum.comm.enums.BasalType
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
+import info.nightscout.pump.medtrum.comm.enums.ModelType
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.util.MedtrumSnUtil
@@ -291,13 +292,13 @@ class MedtrumPump @Inject constructor(
var desiredPumpWarning = true
var desiredPumpWarningExpiryThresholdHours = 72L
- fun pumpType(): PumpType = pumpType(deviceType)
+ fun pumpType(): PumpType = pumpType(ModelType.fromValue(deviceType))
- fun pumpType(type: Int): PumpType =
+ fun pumpType(type: ModelType): PumpType =
when (type) {
- MedtrumSnUtil.MD_0201, MedtrumSnUtil.MD_8201 -> PumpType.MEDTRUM_NANO
- MedtrumSnUtil.MD_8301 -> PumpType.MEDTRUM_300U
- else -> PumpType.MEDTRUM_UNTESTED
+ ModelType.MD0201, ModelType.MD8201 -> PumpType.MEDTRUM_NANO
+ ModelType.MD8301 -> PumpType.MEDTRUM_300U
+ else -> PumpType.MEDTRUM_UNTESTED
}
fun loadVarsFromSP() {
@@ -569,7 +570,7 @@ class MedtrumPump @Inject constructor(
AlarmState.EXPIRED -> R.string.alarm_expired
AlarmState.RESERVOIR_EMPTY -> R.string.alarm_reservoir_empty
AlarmState.PATCH_FAULT -> R.string.alarm_patch_fault
- AlarmState.PATCH_FAULT2 -> R.string.alarm_patch_fault2
+ AlarmState.PATCH_FAULT2 -> R.string.alarm_patch_fault // To avoid confusion, medtrum app also doesn't show patch fault 2
AlarmState.BASE_FAULT -> R.string.alarm_base_fault
AlarmState.BATTERY_OUT -> R.string.alarm_battery_out
AlarmState.NO_CALIBRATION -> R.string.alarm_no_calibration
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/PatchStep.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/PatchStep.kt
index ea0cdf3d28f..401dfa9a97a 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/PatchStep.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/PatchStep.kt
@@ -15,7 +15,6 @@ enum class PatchStep {
ACTIVATE_COMPLETE,
RETRY_ACTIVATION,
RETRY_ACTIVATION_CONNECT,
- ERROR,
CANCEL,
COMPLETE;
}
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/ModelType.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/ModelType.kt
new file mode 100644
index 00000000000..da1afde177d
--- /dev/null
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/ModelType.kt
@@ -0,0 +1,17 @@
+package info.nightscout.pump.medtrum.comm.enums
+
+enum class ModelType(val value: Int) {
+ INVALID(-1),
+ MD0201(80),
+ MD5201(81),
+ MD0202(82),
+ MD5202(83),
+ MD8201(88),
+ MD8301(98);
+
+ companion object {
+ fun fromValue(value: Int): ModelType {
+ return values().find { it.value == value } ?: INVALID
+ }
+ }
+}
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt
index 4d9d52dd382..8a0153ae54c 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt
@@ -132,7 +132,7 @@ class MedtrumService : DaggerService(), BLECommCallback {
if (event.isChanged(rh.gs(R.string.key_sn_input))) {
aapsLogger.debug(LTag.PUMPCOMM, "Serial number changed, reporting new pump!")
medtrumPump.loadUserSettingsFromSP()
- medtrumPump.deviceType = MedtrumSnUtil().getDeviceTypeFromSerial(medtrumPump.pumpSN)
+ medtrumPump.deviceType = MedtrumSnUtil().getDeviceTypeFromSerial(medtrumPump.pumpSN).value
medtrumPump.resetPatchParameters()
pumpSync.connectNewPump()
medtrumPump.setFakeTBRIfNotSet()
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateFragment.kt
index 44df41ee78c..e56b0b800de 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateFragment.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateFragment.kt
@@ -38,7 +38,6 @@ class MedtrumActivateFragment : MedtrumBaseFragment moveStep(PatchStep.ACTIVATE_COMPLETE)
MedtrumViewModel.SetupStep.ERROR -> {
- moveStep(PatchStep.ERROR)
updateSetupStep(MedtrumViewModel.SetupStep.PRIMED) // Reset setup step
binding.textActivatingPump.text = rh.gs(R.string.activating_error)
binding.btnPositive.visibility = View.VISIBLE
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivity.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivity.kt
index 65bec846015..94136e78fd8 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivity.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivity.kt
@@ -43,7 +43,6 @@ class MedtrumActivity : MedtrumBaseActivity() {
PatchStep.ATTACH_PATCH -> setupViewFragment(MedtrumAttachPatchFragment.newInstance())
PatchStep.ACTIVATE -> setupViewFragment(MedtrumActivateFragment.newInstance())
PatchStep.ACTIVATE_COMPLETE -> setupViewFragment(MedtrumActivateCompleteFragment.newInstance())
- PatchStep.ERROR -> Unit // Do nothing, let activity handle this
PatchStep.RETRY_ACTIVATION -> setupViewFragment(MedtrumRetryActivationFragment.newInstance())
PatchStep.RETRY_ACTIVATION_CONNECT -> setupViewFragment(MedtrumRetryActivationConnectFragment.newInstance())
PatchStep.START_DEACTIVATION -> setupViewFragment(MedtrumStartDeactivationFragment.newInstance())
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivatePatchFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivatePatchFragment.kt
index 9201a158da7..cdf039bb5db 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivatePatchFragment.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivatePatchFragment.kt
@@ -38,7 +38,6 @@ class MedtrumDeactivatePatchFragment : MedtrumBaseFragment {
- moveStep(PatchStep.ERROR)
updateSetupStep(MedtrumViewModel.SetupStep.START_DEACTIVATION) // Reset setup step
binding.textDeactivatingPump.text = rh.gs(R.string.deactivating_error)
binding.btnNegative.visibility = View.VISIBLE
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimingFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimingFragment.kt
index 245e4a95265..13012c3eb40 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimingFragment.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimingFragment.kt
@@ -39,7 +39,6 @@ class MedtrumPrimingFragment : MedtrumBaseFragment moveStep(PatchStep.PRIME_COMPLETE)
MedtrumViewModel.SetupStep.ERROR -> {
- moveStep(PatchStep.ERROR)
updateSetupStep(MedtrumViewModel.SetupStep.FILLED) // Reset setup step
binding.textWaitForPriming.text = rh.gs(R.string.priming_error)
binding.btnNegative.visibility = View.VISIBLE
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumOverviewViewModel.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumOverviewViewModel.kt
index decdcf3745e..c01f23fbcea 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumOverviewViewModel.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumOverviewViewModel.kt
@@ -14,6 +14,7 @@ import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.ConnectionState
import info.nightscout.pump.medtrum.code.EventType
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
+import info.nightscout.pump.medtrum.comm.enums.ModelType
import info.nightscout.pump.medtrum.ui.MedtrumBaseNavigator
import info.nightscout.pump.medtrum.ui.event.SingleLiveEvent
import info.nightscout.pump.medtrum.ui.event.UIEvent
@@ -199,7 +200,7 @@ class MedtrumOverviewViewModel @Inject constructor(
val activeAlarmStrings = medtrumPump.activeAlarms.map { medtrumPump.alarmStateToString(it) }
_activeAlarms.postValue(activeAlarmStrings.joinToString("\n"))
- _pumpType.postValue(medtrumPump.deviceType.toString())
+ _pumpType.postValue(ModelType.fromValue(medtrumPump.deviceType).toString())
_fwVersion.postValue(medtrumPump.swVersion)
_patchNo.postValue(medtrumPump.patchId.toString())
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumViewModel.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumViewModel.kt
index a5e38c7c22d..3745ab05e95 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumViewModel.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumViewModel.kt
@@ -137,7 +137,6 @@ class MedtrumViewModel @Inject constructor(
PatchStep.CANCEL,
PatchStep.COMPLETE,
PatchStep.ACTIVATE_COMPLETE,
- PatchStep.ERROR,
PatchStep.START_DEACTIVATION,
PatchStep.DEACTIVATE,
PatchStep.FORCE_DEACTIVATION,
@@ -307,7 +306,6 @@ class MedtrumViewModel @Inject constructor(
PatchStep.COMPLETE,
PatchStep.FORCE_DEACTIVATION,
- PatchStep.ERROR,
PatchStep.CANCEL -> _title.value
}
diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumSnUtil.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumSnUtil.kt
index ad16adc630c..8f21aeef522 100644
--- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumSnUtil.kt
+++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumSnUtil.kt
@@ -1,33 +1,23 @@
package info.nightscout.pump.medtrum.util
+import info.nightscout.pump.medtrum.comm.enums.ModelType
import info.nightscout.pump.medtrum.encryption.Crypt
class MedtrumSnUtil {
- companion object {
-
- const val INVALID = -1
- const val MD_0201 = 80
- const val MD_5201 = 81
- const val MD_0202 = 82
- const val MD_5202 = 83
- const val MD_8201 = 88
- const val MD_8301 = 98
- }
-
- fun getDeviceTypeFromSerial(serial: Long): Int {
+ fun getDeviceTypeFromSerial(serial: Long): ModelType {
if (serial in 106000000..106999999) {
- return INVALID
+ return ModelType.INVALID
}
return when (Crypt().simpleDecrypt(serial)) {
- in 126000000..126999999 -> MD_0201
- in 127000000..127999999 -> MD_5201
- in 128000000..128999999 -> MD_8201
- in 130000000..130999999 -> MD_0202
- in 131000000..131999999 -> MD_5202
- in 148000000..148999999 -> MD_8301
- else -> INVALID
+ in 126000000..126999999 -> ModelType.MD0201
+ in 127000000..127999999 -> ModelType.MD5201
+ in 128000000..128999999 -> ModelType.MD8201
+ in 130000000..130999999 -> ModelType.MD0202
+ in 131000000..131999999 -> ModelType.MD5202
+ in 148000000..148999999 -> ModelType.MD8301
+ else -> ModelType.INVALID
}
}
}
diff --git a/pump/medtrum/src/main/res/values/strings.xml b/pump/medtrum/src/main/res/values/strings.xml
index 10e4496dc83..608c18b4376 100644
--- a/pump/medtrum/src/main/res/values/strings.xml
+++ b/pump/medtrum/src/main/res/values/strings.xml
@@ -77,7 +77,6 @@
Expired
Reservoir empty
Patch fault
- Patch fault 2
Base fault
Battery out
No calibration
@@ -142,7 +141,7 @@
Serial Number
Enter the serial number of your pump base.
Invalid serial number!
- Pump untested: %1$d! Please contact us at discord or github for support
+ Pump untested: %1$s! Please contact us at discord or github for support
Alarm Settings
Select your preferred pump alarm settings.
Notification on pump warning
diff --git a/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/MedtrumPumpTest.kt b/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/MedtrumPumpTest.kt
index 5ba7ff52274..78ca63cfe65 100644
--- a/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/MedtrumPumpTest.kt
+++ b/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/MedtrumPumpTest.kt
@@ -9,6 +9,7 @@ import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress
import app.aaps.core.interfaces.utils.T
import com.google.common.truth.Truth.assertThat
import info.nightscout.pump.medtrum.comm.enums.BasalType
+import info.nightscout.pump.medtrum.comm.enums.ModelType
import info.nightscout.pump.medtrum.util.MedtrumSnUtil
import org.json.JSONObject
import org.junit.jupiter.api.Test
@@ -139,7 +140,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val receivedTime = 1500L
val duration = T.mins(5).msecs()
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
@@ -191,7 +192,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val basalStartTime = 1000L
val receivedTime = 1500L
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
@@ -240,7 +241,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val basalStartTime = 1000L
val receivedTime = 1500L
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
@@ -293,7 +294,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val duration = T.mins(5).msecs()
medtrumPump.actualBasalProfile = medtrumPump.buildMedtrumProfileArray(validProfile)!!
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
@@ -346,7 +347,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val basalStartTime = 1000L
val receivedTime = 1500L
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
@@ -441,7 +442,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val basalStartTime = 1000L
val receivedTime = 1500L
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
@@ -492,7 +493,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val basalStartTime = 1000L
val receivedTime = 1500L
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
Mockito.`when`(pumpSync.expectedPumpState()).thenReturn(
@@ -537,7 +538,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val basalStartTime = 1000L
val receivedTime = 1500L
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
@@ -581,7 +582,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
val basalStartTime = 1000L
val receivedTime = 1500L
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
Mockito.`when`(pumpSync.expectedPumpState()).thenReturn(
@@ -650,7 +651,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
@Test fun setFakeTBRIfNotSetWhenNoFakeTBRAlreadyRunningExpectPumpSync() {
// Inputs
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
@@ -684,7 +685,7 @@ class MedtrumPumpTest : MedtrumTestBase() {
@Test fun setFakeTBRIfNotSetWhenFakeTBRAlreadyRunningExpectNoPumpSync() {
// Inputs
- medtrumPump.deviceType = MedtrumSnUtil.MD_8301
+ medtrumPump.deviceType = ModelType.MD8301.value
// Mocks
val expectedTemporaryBasal: PumpSync.PumpState.TemporaryBasal = mock(PumpSync.PumpState.TemporaryBasal::class.java)
diff --git a/pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml b/pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml
index 12261e0feb2..056fffdff34 100644
--- a/pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml
@@ -124,6 +124,7 @@
Probíhá nastavování (čeká se na aktivaci Podu)
Probíhá nastavování (čeká se na aplikaci kanyly)
Běží
+ Normální
Pozastaveno
Chyba Podu
Byl překročen čas aktivace
diff --git a/pump/omnipod-common/src/main/res/values-es-rES/strings.xml b/pump/omnipod-common/src/main/res/values-es-rES/strings.xml
index 06aa6355035..9ac89e5d5b0 100644
--- a/pump/omnipod-common/src/main/res/values-es-rES/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-es-rES/strings.xml
@@ -125,6 +125,7 @@
Configuración en curso (esperando para activar el Pod)
Configuración en curso (en espera de inserción de cánula)
Funcionando
+ Normal
Suspendido
Error del Pod
Tiempo de activación excedido
diff --git a/pump/omnipod-common/src/main/res/values-fr-rFR/strings.xml b/pump/omnipod-common/src/main/res/values-fr-rFR/strings.xml
index 5582068512e..de3cc9fd837 100644
--- a/pump/omnipod-common/src/main/res/values-fr-rFR/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-fr-rFR/strings.xml
@@ -53,7 +53,7 @@
ID unique
Numéro de lot
Numéro de série
- Pod Expiré
+ Pod expire le
Dernière connexion
Dernier bolus
Débit de Basal Temp.
diff --git a/pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml b/pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml
index 6f0687373e8..7f2172f5274 100644
--- a/pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml
@@ -124,6 +124,7 @@
התקנה בביצוע (ממתין להפעלת הפוד)
התקנה בביצוע (ממתין להכנסת הפרפרית)
פועל
+ נורמלי
מושהה
תקלה בפוד
חריגת זמן הפעלה
diff --git a/pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml b/pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml
index 121dfd1331d..72339fdab6a 100644
--- a/pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml
@@ -124,6 +124,7 @@
Oppsett pågår (venter på Pod-aktivering)
Oppsett pågår (venter på innsetting av kanyle)
Kjører
+ Normal
Pauset
Pod-feil
Aktiveringstiden er overskredet
diff --git a/pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml b/pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml
index 8502534c17c..580524dd47f 100644
--- a/pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml
@@ -124,6 +124,7 @@
Setup wordt uitgevoerd (in afwachting van Pod activering)
Setup wordt uitgevoerd (in afwachting van het inbrengen van de canule)
Actief
+ Normaal
Onderbroken
Pod fout
Activatie tijd verlopen
diff --git a/pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml b/pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml
index 80523e2bdf7..f8ac9ee1276 100644
--- a/pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml
@@ -124,6 +124,7 @@
Konfiguracja w toku (oczekiwanie na aktywację Poda)
Konfiguracja w toku (oczekiwanie na wprowadzenie kaniuli)
W działaniu
+ Normalny
Wstrzymany
Błąd Poda
Przekroczono czas aktywacji
diff --git a/pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml b/pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml
index 08443c0d22d..ad03df49124 100644
--- a/pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml
@@ -125,6 +125,7 @@
Setare în desfășurare (se așteaptă activarea Pod-ului)
Inițializarea este în curs (se așteaptă inserarea canulei)
Rulează
+ Normal
Suspendat
Defecțiune Pod
Timp de activare depăşit
diff --git a/pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml b/pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml
index efa75787828..a04fce5329f 100644
--- a/pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml
@@ -124,6 +124,7 @@
Выполняется настройка (ожидание активации Pod)
Выполняется настройка (ожидание ввода катетера)
Выполняется
+ Норма
Приостановлено
Ошибка Pod
Превышено время активации
diff --git a/pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml b/pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml
index e5a00045428..cb26c7ee907 100644
--- a/pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml
@@ -124,6 +124,7 @@
Prebieha inštalácia (čaká sa na aktiváciu Podu)
Inštalácia prebieha (čaká sa na vloženie kanyly)
V prevádzke
+ Normálny
Pozastavené
Chyba Podu
Bol prekročený čas aktivácie
diff --git a/pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml b/pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml
index 637090f012c..dd668038aa1 100644
--- a/pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml
+++ b/pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml
@@ -124,6 +124,7 @@
Kurulum devam ediyor (Pod aktivasyonu bekleniyor)
Kurulum devam ediyor (kanül yerleştirme bekleniyor)
Çalışıyor
+ Normal
Askıya Alındı
Pod Hatası
Aktivasyon süresi aşıldı
diff --git a/pump/omnipod-common/src/main/res/values/strings.xml b/pump/omnipod-common/src/main/res/values/strings.xml
index d2ecea3ce03..72eb74947aa 100644
--- a/pump/omnipod-common/src/main/res/values/strings.xml
+++ b/pump/omnipod-common/src/main/res/values/strings.xml
@@ -153,6 +153,7 @@
Setup in progress (waiting for Pod activation)
Setup in progress (waiting for cannula insertion)
Running
+ Normal
Suspended
Pod Fault
Activation time exceeded
diff --git a/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt
index 39cd00ef9a0..a0acf389de6 100644
--- a/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt
+++ b/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt
@@ -183,7 +183,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
if (!podStateManager.isPodRunning) {
uiInteraction.addNotification(
Notification.OMNIPOD_POD_NOT_ATTACHED,
- "Pod not activated",
+ rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod),
Notification.NORMAL
)
} else {
@@ -991,9 +991,9 @@ class OmnipodDashPumpPlugin @Inject constructor(
val extended = JSONObject()
try {
val podStatus = when {
- podStateManager.isPodRunning && podStateManager.isSuspended -> "suspended"
- podStateManager.isPodRunning -> "normal"
- else -> "no active Pod"
+ podStateManager.isPodRunning && podStateManager.isSuspended -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_suspended).lowercase()
+ podStateManager.isPodRunning -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_normal).lowercase()
+ else -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod).lowercase()
}
status.put("status", podStatus)
status.put("timestamp", dateUtil.toISOString(podStateManager.lastUpdatedSystem))
diff --git a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt
index 3a170fb5f20..42997cc0ca2 100644
--- a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt
+++ b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt
@@ -36,7 +36,7 @@ import org.mockito.invocation.InvocationOnMock
@Suppress("SpellCheckingInspection")
open class TestBaseWithProfile : TestBase() {
- @Mock lateinit var activePlugin: ActivePlugin
+ @Mock open lateinit var activePlugin: ActivePlugin
@Mock lateinit var rh: ResourceHelper
@Mock lateinit var iobCobCalculator: IobCobCalculator
@Mock lateinit var fabricPrivacy: FabricPrivacy
diff --git a/ui/src/main/res/layout/dialog_wizard.xml b/ui/src/main/res/layout/dialog_wizard.xml
index 885d3204f9d..abf256d80cf 100644
--- a/ui/src/main/res/layout/dialog_wizard.xml
+++ b/ui/src/main/res/layout/dialog_wizard.xml
@@ -284,31 +284,6 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/ui/src/main/res/values-ru-rRU/strings.xml b/ui/src/main/res/values-ru-rRU/strings.xml
index 00cb9201f01..f55731100cf 100644
--- a/ui/src/main/res/values-ru-rRU/strings.xml
+++ b/ui/src/main/res/values-ru-rRU/strings.xml
@@ -42,7 +42,7 @@
Внести запись о смене места катетера помпы
Внести запись о замене картриджа инсулина
- Не подавать болюс, только внести запись
+ Не вводить болюс, только внести запись
Повторно использовать %1$d%%%2$dч
Сдвиг по времени
diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts
index ad13b8e525c..ce0b3e45393 100644
--- a/wear/build.gradle.kts
+++ b/wear/build.gradle.kts
@@ -86,6 +86,9 @@ android {
versionName = Versions.appVersion + "-aapsclient2"
}
}
+ buildFeatures {
+ buildConfig = true
+ }
}
allprojects {
diff --git a/wear/src/main/res/values-ru-rRU/strings.xml b/wear/src/main/res/values-ru-rRU/strings.xml
index e91a4e0d36a..c679e5131de 100644
--- a/wear/src/main/res/values-ru-rRU/strings.xml
+++ b/wear/src/main/res/values-ru-rRU/strings.xml
@@ -116,7 +116,7 @@
ПОДТВЕРДИТЬ
сдвиг по времени
Болюс
- Подается болюс
+ Вводится болюс
нажмите для отмены
ОТМЕНИТЬ БОЛЮС
Помпа
@@ -161,10 +161,10 @@
Ваш цвет:
Ваша цветовая насыщенность:
Непрозрачность вашего цвета:
- Подается болюс
- Беззвучная подача болюса
- Подача и отмена болюса
- Подача и отмена болюса с меньшими вибрациями
+ Вводится болюс
+ Беззвучный ввод болюса
+ Ввод и отмена болюса
+ Введение и отмена болюса с меньшими вибрациями
Выкл
Во время зарядки
Режим: всегда вкл
@@ -192,7 +192,7 @@
000г
00,0
0,00 ед
- Оповещение о переходе AAPS в режим энергосбережения
+ Отложить оповещения AAPS
Отправка команды отложить оповещения AAPS
ч
н