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 ч н