Merge pull request #3098 from nightscout/dev

3.2.0.3
This commit is contained in:
Milos Kozak 2023-12-16 20:37:23 +01:00 committed by GitHub
commit bb1a276078
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 2700 additions and 217 deletions

View file

@ -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 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: orbs:
android: circleci/android@2.3.0 android: circleci/android@2.3.0
codecov: codecov/codecov@3.2.4 codecov: codecov/codecov@3.3.0
jobs: jobs:
# Below is the definition of your job to build and test your app, you can rename and customize it as you want. # 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: executor:
name: android/android-machine name: android/android-machine
resource-class: large resource-class: large
tag: 2023.07.1 tag: 2023.11.1
steps: steps:
- checkout - checkout
@ -34,11 +34,19 @@ jobs:
- android/run-tests: - android/run-tests:
test-command: ./gradlew --stacktrace jacocoAllDebugReport test-command: ./gradlew --stacktrace jacocoAllDebugReport
# And finally run the release build - run:
# - run: name: Save test results
# name: Assemble release build command: |
# command: | mkdir -p ~/test-results/junit/
# ./gradlew assembleRelease 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: - codecov/upload:
file: './build/reports/jacoco/jacocoAllDebugReport/jacocoAllDebugReport.xml' file: './build/reports/jacoco/jacocoAllDebugReport/jacocoAllDebugReport.xml'

View file

@ -153,6 +153,7 @@ android {
//Deleting it causes a binding error //Deleting it causes a binding error
buildFeatures { buildFeatures {
dataBinding = true dataBinding = true
buildConfig = true
} }
} }

View file

@ -276,7 +276,7 @@ class MainActivity : DaggerAppCompatActivityWithResult() {
}) })
// Setup views on 2nd and next activity start // Setup views on 2nd and next activity start
// On 1st start app is still initializing, start() is delayed and run from EventAppInitialized // 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() { private fun start() {

View file

@ -47,7 +47,7 @@ import app.aaps.plugins.source.PoctechPlugin
import app.aaps.plugins.source.RandomBgPlugin import app.aaps.plugins.source.RandomBgPlugin
import app.aaps.plugins.source.TomatoPlugin import app.aaps.plugins.source.TomatoPlugin
import app.aaps.plugins.source.XdripSourcePlugin 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.nsclient.NSClientPlugin
import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin
import app.aaps.plugins.sync.openhumans.OpenHumansUploaderPlugin import app.aaps.plugins.sync.openhumans.OpenHumansUploaderPlugin
@ -350,7 +350,7 @@ abstract class PluginsListModule {
@AllConfigs @AllConfigs
@IntoMap @IntoMap
@IntKey(368) @IntKey(368)
abstract fun bindDataBroadcastPlugin(plugin: DataBroadcastPlugin): PluginBase abstract fun bindDataBroadcastPlugin(plugin: TizenPlugin): PluginBase
@Binds @Binds
@AllConfigs @AllConfigs

View file

@ -8,7 +8,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:8.1.3") classpath("com.android.tools.build:gradle:8.2.0")
classpath("com.google.gms:google-services:4.4.0") classpath("com.google.gms:google-services:4.4.0")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9") classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9")
@ -22,7 +22,7 @@ buildscript {
} }
plugins { plugins {
id("org.jlleitschuh.gradle.ktlint") version "11.6.1" id("org.jlleitschuh.gradle.ktlint") version "12.0.3"
} }
allprojects { allprojects {

View file

@ -1,7 +1,7 @@
object KtsBuildVersions { object KtsBuildVersions {
const val gradle = "8.1.3" const val gradle = "8.2.0"
const val kotlin = "1.9.0" const val kotlin = "1.9.10"
} }
plugins { plugins {

View file

@ -152,8 +152,11 @@ object Libs {
} }
object Mockito { 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" 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 commonCodecs = "commons-codec:commons-codec:1.16.0"
const val kulid = "com.github.guepardoapps:kulid:2.0.0.0" const val kulid = "com.github.guepardoapps:kulid:2.0.0.0"
const val xstream = "com.thoughtworks.xstream:xstream:1.4.20" const val xstream = "com.thoughtworks.xstream:xstream:1.4.20"
const val connectiqSdk = "com.garmin.connectiq:ciq-companion-app-sdk:2.0.3@aar"
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"
} }

View file

@ -2,7 +2,7 @@ import org.gradle.api.JavaVersion
object Versions { object Versions {
const val appVersion = "3.2.0.2" const val appVersion = "3.2.0.3"
const val versionCode = 1500 const val versionCode = 1500
const val ndkVersion = "21.1.6352462" const val ndkVersion = "21.1.6352462"

View file

@ -22,6 +22,9 @@ dependencies {
androidTestImplementation(Libs.AndroidX.Test.rules) androidTestImplementation(Libs.AndroidX.Test.rules)
androidTestImplementation(Libs.Google.truth) androidTestImplementation(Libs.Google.truth)
androidTestImplementation(Libs.AndroidX.Test.uiAutomator) androidTestImplementation(Libs.AndroidX.Test.uiAutomator)
androidTestImplementation(Libs.Mockito.core)
androidTestImplementation(Libs.Mockito.android)
androidTestImplementation(Libs.Mockito.kotlin)
} }
tasks.withType<Test> { tasks.withType<Test> {

View file

@ -1,5 +1,6 @@
package app.aaps.core.interfaces.pump 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.profile.Profile
import app.aaps.core.interfaces.pump.defs.PumpType import app.aaps.core.interfaces.pump.defs.PumpType
import app.aaps.core.interfaces.utils.DateUtil 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 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 * Create an announcement
* *

View file

@ -25,6 +25,7 @@ interface Intents {
const val EXTRA_BG_SLOPE = "com.eveningoutpost.dexdrip.Extras.BgSlope" const val EXTRA_BG_SLOPE = "com.eveningoutpost.dexdrip.Extras.BgSlope"
const val EXTRA_BG_SLOPE_NAME = "com.eveningoutpost.dexdrip.Extras.BgSlopeName" 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_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_TIMESTAMP = "com.eveningoutpost.dexdrip.Extras.Time"
const val EXTRA_RAW = "com.eveningoutpost.dexdrip.Extras.Raw" const val EXTRA_RAW = "com.eveningoutpost.dexdrip.Extras.Raw"
const val XDRIP_DATA_SOURCE_DESCRIPTION = "com.eveningoutpost.dexdrip.Extras.SourceDesc" const val XDRIP_DATA_SOURCE_DESCRIPTION = "com.eveningoutpost.dexdrip.Extras.SourceDesc"

View file

@ -1,22 +1,56 @@
package app.aaps.core.utils 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 * 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 { fun secondsFromMidnight(): Int {
val passed = DateTime().millisOfDay.toLong() val nowZoned = ZonedDateTime.now()
return (passed / 1000).toInt() 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() * Passed seconds from midnight for specified time ignoring DST change
return (passed / 1000).toInt() * (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()
} }
} }

View file

@ -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)
}
}

View file

@ -17,7 +17,7 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.warning.mode=all org.gradle.warning.mode=all
org.gradle.jvmargs=-Xmx3g -XX:+UseParallelGC org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
android.enableJetifier=false android.enableJetifier=false
android.useAndroidX=true android.useAndroidX=true
@ -32,5 +32,4 @@ android.nonTransitiveRClass=true
# null: KtCallExpression # null: KtCallExpression
# https://youtrack.jetbrains.com/issue/KT-58027 # https://youtrack.jetbrains.com/issue/KT-58027
kapt.use.jvm.ir=false kapt.use.jvm.ir=false
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=true android.nonFinalResIds=true

View file

@ -1,5 +1,6 @@
package app.aaps.implementation.pump 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.AAPSLogger
import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.logging.UserEntryLogger 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.DateUtil
import app.aaps.core.interfaces.utils.T import app.aaps.core.interfaces.utils.T
import app.aaps.core.main.events.EventNewNotification 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.fromDbPumpType
import app.aaps.core.main.pump.toDbPumpType import app.aaps.core.main.pump.toDbPumpType
import app.aaps.core.main.pump.toDbSource 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) { override fun insertAnnouncement(error: String, pumpId: Long?, pumpType: PumpType, pumpSerial: String) {
if (!confirmActivePump(dateUtil.now(), pumpType, pumpSerial)) return if (!confirmActivePump(dateUtil.now(), pumpType, pumpSerial)) return
disposable += repository.runTransaction(InsertTherapyEventAnnouncementTransaction(error, pumpId, pumpType.toDbPumpType(), pumpSerial)) disposable += repository.runTransaction(InsertTherapyEventAnnouncementTransaction(error, pumpId, pumpType.toDbPumpType(), pumpSerial))

View file

@ -119,13 +119,13 @@ class CommandQueueImplementation @Inject constructor(
return@subscribe return@subscribe
} }
aapsLogger.debug(LTag.PROFILE, "onEventProfileSwitchChanged") aapsLogger.debug(LTag.PROFILE, "onEventProfileSwitchChanged")
val effective = repository.getEffectiveProfileSwitchActiveAt(dateUtil.now()).blockingGet()
profileFunction.getRequestedProfile()?.let { profileFunction.getRequestedProfile()?.let {
setProfile(ProfileSealed.PS(it), it.interfaceIDs.nightscoutId != null, object : Callback() { setProfile(ProfileSealed.PS(it), it.interfaceIDs.nightscoutId != null, object : Callback() {
override fun run() { override fun run() {
if (!result.success) { 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) 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) val nonCustomized = ProfileSealed.PS(it).convertToNonCustomizedProfile(dateUtil)
EffectiveProfileSwitch( EffectiveProfileSwitch(
timestamp = dateUtil.now(), timestamp = dateUtil.now(),

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="alert_r1_title">Дать болюс</string> <string name="alert_r1_title">Ввести болюс</string>
<string name="alert_r2_title">Недоставленный болюс</string> <string name="alert_r2_title">Несостоявшийся болюс</string>
<string name="alert_r3_title">Будильник</string> <string name="alert_r3_title">Будильник</string>
<string name="alert_r4_title">Замените инфузионный набор</string> <string name="alert_r4_title">Замените инфузионный набор</string>
<string name="alert_r7_title">TBR завершен</string> <string name="alert_r7_title">TBR завершен</string>

View file

@ -79,6 +79,7 @@ import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.plusAssign
import org.json.JSONObject import org.json.JSONObject
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.abs import kotlin.math.abs
@ -135,6 +136,8 @@ class LoopPlugin @Inject constructor(
disposable += rxBus disposable += rxBus
.toObservable(EventTempTargetChange::class.java) .toObservable(EventTempTargetChange::class.java)
.observeOn(aapsSchedulers.io) .observeOn(aapsSchedulers.io)
// Skip db change of ending previous TT
.debounce(10L, TimeUnit.SECONDS)
.subscribe({ invoke("EventTempTargetChange", true) }, fabricPrivacy::logException) .subscribe({ invoke("EventTempTargetChange", true) }, fabricPrivacy::logException)
} }

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="dynisf_adjust_sensitivity">Habilitar la relación de sensibilidad basada en TDD para modificar las basales y el objetivo de glucosa</string> <string name="dynisf_adjust_sensitivity">Habilitar la relación de sensibilidad basada en TDD para modificar las basales y el objetivo de glucosa</string>
<string name="dynisf_adjust_sensitivity_summary">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</string> <string name="dynisf_adjust_sensitivity_summary">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</string>
<string name="DynISFAdjust_title" formatted="false">Factor de ajuste de ISF Dinámico %</string> <string name="DynISFAdjust_title" formatted="false">Factor de ajuste de ISF Dinámico %</string>
<string name="DynISFAdjust_summary" formatted="false">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.</string> <string name="DynISFAdjust_summary" formatted="false">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.</string>
<string name="high_temptarget_raises_sensitivity_title">Objetivo temporal alto aumenta la sensibilidad</string> <string name="high_temptarget_raises_sensitivity_title">Objetivo temporal alto aumenta la sensibilidad</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="dynisf_adjust_sensitivity">Activer le ratio de sensibilité basé sur DTQ pour la modification du basal et la cible des glycémies</string>
<string name="dynisf_adjust_sensitivity_summary">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</string>
<string name="DynISFAdjust_title" formatted="false">Facteur d\'ajustement Si dynamique %</string> <string name="DynISFAdjust_title" formatted="false">Facteur d\'ajustement Si dynamique %</string>
<string name="DynISFAdjust_summary" formatted="false">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.</string> <string name="DynISFAdjust_summary" formatted="false">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.</string>
<string name="high_temptarget_raises_sensitivity_title">Cible temp. haute élève la sensibilité</string> <string name="high_temptarget_raises_sensitivity_title">Cible temp. haute élève la sensibilité</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="dynisf_adjust_sensitivity">אפשר יחס רגישות המבוסס על המינון היומי הכולל לשינוי מינון בזאלי וערכי מטרה</string>
<string name="dynisf_adjust_sensitivity_summary">השתמש במינון יומי כולל של 24 שעות\\7 ימים האחרונים כדי לחשב יחס רגישות לצורך העלאה או הורדה של המינון הבזאלי ובנוסף להתאים את ערך המטרה אם אפשרויות אלו מופעלותת באופן דומה ל-Autosense. מומלץ להתחיל כשאפשרות זו כבויה</string>
<string name="DynISFAdjust_title" formatted="false">כיוונון פקטור הרגישות הדינאמית %</string> <string name="DynISFAdjust_title" formatted="false">כיוונון פקטור הרגישות הדינאמית %</string>
<string name="DynISFAdjust_summary" formatted="false">כיוונון פקטור הרגישות הדינאמית. הגדירו יותר מ-100% לקבלת תיקונים אגרסיביים יותר ופחות מ-100% לקבלת תיקונים עדינים יותר.</string> <string name="DynISFAdjust_summary" formatted="false">כיוונון פקטור הרגישות הדינאמית. הגדירו יותר מ-100% לקבלת תיקונים אגרסיביים יותר ופחות מ-100% לקבלת תיקונים עדינים יותר.</string>
<string name="high_temptarget_raises_sensitivity_title">ערך מטרה זמני גבוה מעלה את הרגישות</string> <string name="high_temptarget_raises_sensitivity_title">ערך מטרה זמני גבוה מעלה את הרגישות</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="dynisf_adjust_sensitivity">Inschakelen van TDD gebaseerde gevoeligheid ratio voor basaal en glucose doel aanpassingen</string>
<string name="dynisf_adjust_sensitivity_summary">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</string>
<string name="DynISFAdjust_title" formatted="false">Dynamische Isf aanpassingsfactor %</string> <string name="DynISFAdjust_title" formatted="false">Dynamische Isf aanpassingsfactor %</string>
<string name="DynISFAdjust_summary" formatted="false">Aanpassingsfactor voor DynamicISF. Stel meer dan 100% in voor een agressievere correctie en minder dan 100% voor minder agressieve correctie.</string> <string name="DynISFAdjust_summary" formatted="false">Aanpassingsfactor voor DynamicISF. Stel meer dan 100% in voor een agressievere correctie en minder dan 100% voor minder agressieve correctie.</string>
<string name="high_temptarget_raises_sensitivity_title">Hoog tijdelijk streefdoel verhoogt gevoeligheid</string> <string name="high_temptarget_raises_sensitivity_title">Hoog tijdelijk streefdoel verhoogt gevoeligheid</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="dynisf_adjust_sensitivity">Activează raportul de sensibilitate bazat pe TDD pentru modificarea bazalei și a țintei glicemice</string>
<string name="dynisf_adjust_sensitivity_summary">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</string>
<string name="DynISFAdjust_title" formatted="false">Factor de ajustare dinamic %</string> <string name="DynISFAdjust_title" formatted="false">Factor de ajustare dinamic %</string>
<string name="DynISFAdjust_summary" formatted="false">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.</string> <string name="DynISFAdjust_summary" formatted="false">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.</string>
<string name="high_temptarget_raises_sensitivity_title">Ținte temporare mai mari cresc sensibilitatea</string> <string name="high_temptarget_raises_sensitivity_title">Ținte temporare mai mari cresc sensibilitatea</string>

View file

@ -10,9 +10,9 @@
<string name="low_temptarget_lowers_sensitivity_summary"><![CDATA[Понизить чувствительность для temptargets > = 100]]></string> <string name="low_temptarget_lowers_sensitivity_summary"><![CDATA[Понизить чувствительность для temptargets > = 100]]></string>
<string name="openapssmb">OpenAPS СМБ</string> <string name="openapssmb">OpenAPS СМБ</string>
<string name="openaps_smb_dynamic_isf">Динамический ISF</string> <string name="openaps_smb_dynamic_isf">Динамический ISF</string>
<string name="smb_interval_summary">Как часто СМБ будут подаваться в минутах</string> <string name="smb_interval_summary">Как часто SMB будут подаваться в минутах</string>
<string name="resistance_lowers_target_title">Сопротивляемость понижает цель</string> <string name="resistance_lowers_target_title">Резистентность понижает цель</string>
<string name="resistance_lowers_target_summary">При обнаружении сопротивляемости целевой уровень гликемии понижается</string> <string name="resistance_lowers_target_summary">При обнаружении резистентности целевой уровень гликемии понижается</string>
<string name="sensitivity_raises_target_title">Чувствительность поднимает цель</string> <string name="sensitivity_raises_target_title">Чувствительность поднимает цель</string>
<string name="sensitivity_raises_target_summary">При обнаружении чувствительности целевой уровень глюкозы повышается</string> <string name="sensitivity_raises_target_summary">При обнаружении чувствительности целевой уровень глюкозы повышается</string>
<string name="openapsma_disabled">модуль не активен</string> <string name="openapsma_disabled">модуль не активен</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="dynisf_adjust_sensitivity">Povoliť, aby citlivosť založená na CDD mohla upravovať bazál a cieľovú glykémiu</string>
<string name="dynisf_adjust_sensitivity_summary">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úť.</string>
<string name="DynISFAdjust_title" formatted="false">Korekčný faktor pre Dynamickú ISF v %</string> <string name="DynISFAdjust_title" formatted="false">Korekčný faktor pre Dynamickú ISF v %</string>
<string name="DynISFAdjust_summary" formatted="false">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%.</string> <string name="DynISFAdjust_summary" formatted="false">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%.</string>
<string name="high_temptarget_raises_sensitivity_title">Vysoký dočasný cieľ zvýši citlivosť</string> <string name="high_temptarget_raises_sensitivity_title">Vysoký dočasný cieľ zvýši citlivosť</string>

View file

@ -35,7 +35,7 @@
<string name="diawarning">Напоминание: новые профили инсулина требуют как минимум 5 часов длительности действия DIA. 5-6 часовая DIA эквивалентна трехчасовой на старых профилях.</string> <string name="diawarning">Напоминание: новые профили инсулина требуют как минимум 5 часов длительности действия DIA. 5-6 часовая DIA эквивалентна трехчасовой на старых профилях.</string>
<string name="profile">Профиль</string> <string name="profile">Профиль</string>
<string name="doprofileswitch">Переключить профиль</string> <string name="doprofileswitch">Переключить профиль</string>
<string name="exitwizard">Пропустить Мастер установки</string> <string name="exitwizard">Пропустить Мастер настройки</string>
<string name="setupwizard_loop_description">Нажмите на кнопку ниже, чтобы AAPS мог предложить/внести изменения в базал</string> <string name="setupwizard_loop_description">Нажмите на кнопку ниже, чтобы AAPS мог предложить/внести изменения в базал</string>
<string name="setupwizard_sensitivity_description">Плагин чувствительности Sensitivity применяется для определения чувствительности к инсулину и вычисления активных углеводов COB. Дополнительная информация:</string> <string name="setupwizard_sensitivity_description">Плагин чувствительности Sensitivity применяется для определения чувствительности к инсулину и вычисления активных углеводов COB. Дополнительная информация:</string>
<string name="setupwizard_sensitivity_url">https://wiki.aaps.app/en/latest/Configuration/Sensitivity-detection-and-COB.html</string> <string name="setupwizard_sensitivity_url">https://wiki.aaps.app/en/latest/Configuration/Sensitivity-detection-and-COB.html</string>

View file

@ -1,9 +1,11 @@
package app.aaps.plugins.constraints.versionChecker package app.aaps.plugins.constraints.versionChecker
import org.joda.time.LocalDate
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
class AllowedVersions { class AllowedVersions {
@ -38,7 +40,8 @@ class AllowedVersions {
fun endDateToMilliseconds(endDate: String): Long? { fun endDateToMilliseconds(endDate: String): Long? {
try { try {
val dateTime = LocalDate.parse(endDate) 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) { } catch (ignored: Exception) {
} }
return null return null

View file

@ -16,7 +16,7 @@
<string name="version_expire">Versjon %1$s utløper den %2$s</string> <string name="version_expire">Versjon %1$s utløper den %2$s</string>
<!-- Signature verifier --> <!-- Signature verifier -->
<!-- BG Quality --> <!-- BG Quality -->
<string name="recalculated_data_used">Bruker rekalkulerte data</string> <string name="recalculated_data_used">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</string>
<string name="bg_too_close">BS for nær:\n%1$s\n%2$s</string> <string name="bg_too_close">BS for nær:\n%1$s\n%2$s</string>
<string name="a11y_bg_quality_recalculated">beregnet på nytt</string> <string name="a11y_bg_quality_recalculated">beregnet på nytt</string>
<string name="a11y_bg_quality_doubles">doble registreringer</string> <string name="a11y_bg_quality_doubles">doble registreringer</string>

View file

@ -2,7 +2,7 @@
<resources> <resources>
<string name="objectives_button_start">Start</string> <string name="objectives_button_start">Start</string>
<string name="objectives_button_verify">Zweryfikuj</string> <string name="objectives_button_verify">Zweryfikuj</string>
<string name="nth_objective">%1$d. Cel</string> <string name="nth_objective">Zadanie %1$d.</string>
<string name="objectivenotstarted">Zadanie %1$d nierozpoczęte</string> <string name="objectivenotstarted">Zadanie %1$d nierozpoczęte</string>
<string name="objectivenotfinished">Zadanie %1$d nieukończone</string> <string name="objectivenotfinished">Zadanie %1$d nieukończone</string>
<string name="objectives_0_objective">Konfiguracja wizualizacji i monitorowania, analiza dawek bazowych i współczynników</string> <string name="objectives_0_objective">Konfiguracja wizualizacji i monitorowania, analiza dawek bazowych i współczynników</string>

View file

@ -12,7 +12,7 @@
<string name="objectives_openloop_gate">Используйте режим Открытого цикла на протяжении нескольких дней и вручную выставляйте временные цели. Настройте ВЦ по умолчанию (ВЦ для Нагрузки, Гипо, Ожидания приема пищи) и используйте их.</string> <string name="objectives_openloop_gate">Используйте режим Открытого цикла на протяжении нескольких дней и вручную выставляйте временные цели. Настройте ВЦ по умолчанию (ВЦ для Нагрузки, Гипо, Ожидания приема пищи) и используйте их.</string>
<string name="objectives_openloop_learned">Открытый цикл может быть использован для получения рекомендаций по терапии, если у вас нет совместимой помпы или если вы не готовы закрыть цикл.</string> <string name="objectives_openloop_learned">Открытый цикл может быть использован для получения рекомендаций по терапии, если у вас нет совместимой помпы или если вы не готовы закрыть цикл.</string>
<string name="objectives_maxbasal_objective">Глубже понимаем работу открытого цикла, включая рекомендации по ВБС</string> <string name="objectives_maxbasal_objective">Глубже понимаем работу открытого цикла, включая рекомендации по ВБС</string>
<string name="objectives_maxbasal_gate">На основе накопленного опыта, определяем максимальную величину базала и задаем ее в помпе и в настройки AndroidAPS</string> <string name="objectives_maxbasal_gate">На основе накопленного опыта, определяем максимальную величину базала и задаем ее в помпе и в настройках AAPS</string>
<string name="objectives_maxbasal_learned">Примите меры предосторожности и корректируйте, если необходимо, параметры безопасности.</string> <string name="objectives_maxbasal_learned">Примите меры предосторожности и корректируйте, если необходимо, параметры безопасности.</string>
<string name="objectives_maxiobzero_objective">Начинаем замыкать цикл с прекращением подачи инсулина при низких значениях ГК (режим Low Glucose Suspend)</string> <string name="objectives_maxiobzero_objective">Начинаем замыкать цикл с прекращением подачи инсулина при низких значениях ГК (режим Low Glucose Suspend)</string>
<string name="objectives_maxiobzero_gate">Работа в замкнутом цикле с maxIOB = 0 на протяжении нескольких дней. Старайтесь избегать низкой ГК, чтобы не вызывать события приостановки подачи инсулина на низких сахарах.</string> <string name="objectives_maxiobzero_gate">Работа в замкнутом цикле с maxIOB = 0 на протяжении нескольких дней. Старайтесь избегать низкой ГК, чтобы не вызывать события приостановки подачи инсулина на низких сахарах.</string>

View file

@ -1,11 +1,12 @@
package app.aaps.plugins.constraints.versionChecker package app.aaps.plugins.constraints.versionChecker
import com.google.common.truth.Truth.assertThat 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.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
class AllowedVersionsTest { class AllowedVersionsTest {
@ -71,10 +72,10 @@ class AllowedVersionsTest {
@Test @Test
fun endDateToMilliseconds() { fun endDateToMilliseconds() {
val definition = generateSupportedVersions() val definition = generateSupportedVersions()
val endDate = AllowedVersions().endDateToMilliseconds(AllowedVersions().findByVersion(definition, "2.9.0-beta1")?.getString("endDate") ?: "1000/01/01") val endDate = AllowedVersions().endDateToMilliseconds(AllowedVersions().findByVersion(definition, "2.9.0-beta1")?.getString("endDate") ?: "1000/01/01") ?: 0L
val dateTime = LocalDate(endDate) val dateTime = LocalDate.ofInstant(Instant.ofEpochMilli(endDate), ZoneId.systemDefault())
assertThat(dateTime.year).isEqualTo(2021) assertThat(dateTime.year).isEqualTo(2021)
assertThat(dateTime.monthOfYear).isEqualTo(11) assertThat(dateTime.monthValue).isEqualTo(11)
assertThat(dateTime.dayOfMonth).isEqualTo(7) assertThat(dateTime.dayOfMonth).isEqualTo(7)
assertThat(AllowedVersions().endDateToMilliseconds("abdef")).isNull() assertThat(AllowedVersions().endDateToMilliseconds("abdef")).isNull()

View file

@ -124,7 +124,7 @@ class PersistentNotificationPlugin @Inject constructor(
val lastBG = iobCobCalculator.ads.lastBg() val lastBG = iobCobCalculator.ads.lastBg()
val glucoseStatus = glucoseStatusProvider.glucoseStatusData val glucoseStatus = glucoseStatusProvider.glucoseStatusData
if (lastBG != null) { if (lastBG != null) {
line1aa = profileUtil.fromMgdlToStringInUnits(lastBG.value) line1aa = profileUtil.fromMgdlToStringInUnits(lastBG.recalculated)
line1 = line1aa line1 = line1aa
if (glucoseStatus != null) { if (glucoseStatus != null) {
line1 += (" Δ" + profileUtil.fromMgdlToSignedStringInUnits(glucoseStatus.delta) line1 += (" Δ" + profileUtil.fromMgdlToSignedStringInUnits(glucoseStatus.delta)

View file

@ -356,11 +356,11 @@ class SmsCommunicatorPlugin @Inject constructor(
var reply = "" var reply = ""
val units = profileUtil.units val units = profileUtil.units
if (actualBG != null) { 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) { } else if (lastBG != null) {
val agoMilliseconds = dateUtil.now() - lastBG.timestamp val agoMilliseconds = dateUtil.now() - lastBG.timestamp
val agoMin = (agoMilliseconds / 60.0 / 1000.0).toInt() 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 val glucoseStatus = glucoseStatusProvider.glucoseStatusData
if (glucoseStatus != null) reply += rh.gs(R.string.sms_delta) + " " + profileUtil.fromMgdlToUnits(glucoseStatus.delta) + " " + units + ", " if (glucoseStatus != null) reply += rh.gs(R.string.sms_delta) + " " + profileUtil.fromMgdlToUnits(glucoseStatus.delta) + " " + units + ", "

View file

@ -300,7 +300,7 @@ class ProfileFragment : DaggerFragment() {
binding.profileRemove.setOnClickListener { binding.profileRemove.setOnClickListener {
activity?.let { activity -> 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( uel.log(
UserEntry.Action.PROFILE_REMOVED, UserEntry.Sources.LocalProfile, ValueWithUnit.SimpleString( UserEntry.Action.PROFILE_REMOVED, UserEntry.Sources.LocalProfile, ValueWithUnit.SimpleString(
profilePlugin.currentProfile()?.name profilePlugin.currentProfile()?.name

View file

@ -211,7 +211,7 @@
<string name="change_your_input">Endre dine inndata!</string> <string name="change_your_input">Endre dine inndata!</string>
<string name="openaps">OpenAPS</string> <string name="openaps">OpenAPS</string>
<string name="uploader">Opplaster-batteri</string> <string name="uploader">Opplaster-batteri</string>
<string name="data_status">BS data status</string> <string name="data_status">Status BS-data</string>
<string name="quickwizard_settings">Innstillinger for hurtigknapp</string> <string name="quickwizard_settings">Innstillinger for hurtigknapp</string>
<string name="keep_screen_on_title">Hold skjermen påslått</string> <string name="keep_screen_on_title">Hold skjermen påslått</string>
<string name="keep_screen_on_summary">Forhindre Android fra å slå av skjermen. Mobilen vil bruke mye batteri hvis den ikke kobles til strømledning.</string> <string name="keep_screen_on_summary">Forhindre Android fra å slå av skjermen. Mobilen vil bruke mye batteri hvis den ikke kobles til strømledning.</string>

View file

@ -2,9 +2,9 @@
<resources> <resources>
<string name="smoothing_shortname">UTJEVNING</string> <string name="smoothing_shortname">UTJEVNING</string>
<string name="exponential_smoothing_name">Eksponentiell utjevning</string> <string name="exponential_smoothing_name">Eksponentiell utjevning</string>
<string name="description_exponential_smoothing">"Andre ordens algoritme for eksponentiell utjevning"</string> <string name="description_exponential_smoothing">"Algoritme for eksponentiell utjevning, nyeste BS-verdi påvirkes"</string>
<string name="avg_smoothing_name">Gjennomsnittlig utjevning</string> <string name="avg_smoothing_name">Gjennomsnittlig utjevning</string>
<string name="description_avg_smoothing">"Gjennomsnittlig utjevnings-algoritme, nyeste verdi påvirkes ikke"</string> <string name="description_avg_smoothing">"Algoritme for gjennomsnittlig utjevning, nyeste BS-verdi påvirkes ikke. Kan minne om BYODA G6 sin utjevningsalgoritme"</string>
<string name="no_smoothing_name">Ingen utjevning</string> <string name="no_smoothing_name">Ingen utjevning</string>
<string name="description_no_smoothing">"Ingen utjevning utføres på motatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6."</string> <string name="description_no_smoothing">"Ingen utjevning utføres på mottatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6."</string>
</resources> </resources>

View file

@ -22,6 +22,8 @@ dependencies {
implementation(project(":core:ui")) implementation(project(":core:ui"))
implementation(project(":core:utils")) implementation(project(":core:utils"))
testImplementation(Libs.AndroidX.Work.testing)
testImplementation(project(":shared:tests")) testImplementation(project(":shared:tests"))
kapt(Libs.Dagger.compiler) kapt(Libs.Dagger.compiler)

View file

@ -1,20 +1,28 @@
package app.aaps.plugins.source package app.aaps.plugins.source
import android.content.Context import android.content.Context
import android.os.Bundle
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag 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.PluginBase
import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.plugin.PluginDescription
import app.aaps.core.interfaces.plugin.PluginType import app.aaps.core.interfaces.plugin.PluginType
import app.aaps.core.interfaces.receivers.Intents import app.aaps.core.interfaces.receivers.Intents
import app.aaps.core.interfaces.resources.ResourceHelper 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.BgSource
import app.aaps.core.interfaces.source.XDripSource 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.main.utils.worker.LoggingWorker
import app.aaps.core.utils.receivers.DataWorkerStorage import app.aaps.core.utils.receivers.DataWorkerStorage
import app.aaps.database.entities.GlucoseValue 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.AppRepository
import app.aaps.database.impl.transactions.CgmSourceTransaction import app.aaps.database.impl.transactions.CgmSourceTransaction
import app.aaps.database.transactions.TransactionGlucoseValue import app.aaps.database.transactions.TransactionGlucoseValue
@ -22,6 +30,7 @@ import dagger.android.HasAndroidInjector
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.abs
import kotlin.math.round import kotlin.math.round
@Singleton @Singleton
@ -34,7 +43,7 @@ class XdripSourcePlugin @Inject constructor(
.mainType(PluginType.BGSOURCE) .mainType(PluginType.BGSOURCE)
.fragmentClass(BGSourceFragment::class.java.name) .fragmentClass(BGSourceFragment::class.java.name)
.pluginIcon((app.aaps.core.main.R.drawable.ic_blooddrop_48)) .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) .pluginName(R.string.source_xdrip)
.description(R.string.description_source_xdrip), .description(R.string.description_source_xdrip),
aapsLogger, rh, injector aapsLogger, rh, injector
@ -63,8 +72,25 @@ class XdripSourcePlugin @Inject constructor(
) : LoggingWorker(context, params, Dispatchers.IO) { ) : LoggingWorker(context, params, Dispatchers.IO) {
@Inject lateinit var xdripSourcePlugin: XdripSourcePlugin @Inject lateinit var xdripSourcePlugin: XdripSourcePlugin
@Inject lateinit var sp: SP
@Inject lateinit var dateUtil: DateUtil
@Inject lateinit var repository: AppRepository @Inject lateinit var repository: AppRepository
@Inject lateinit var dataWorkerStorage: DataWorkerStorage @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 { override suspend fun doWorkAndLog(): Result {
var ret = Result.success() var ret = Result.success()
@ -86,17 +112,27 @@ class XdripSourcePlugin @Inject constructor(
?: "" ?: ""
) )
) )
repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), null)) val sensorStartTime = getSensorStartTime(bundle)
repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), sensorStartTime))
.doOnError { .doOnError {
aapsLogger.error(LTag.DATABASE, "Error while saving values from Xdrip", it) aapsLogger.error(LTag.DATABASE, "Error while saving values from Xdrip", it)
ret = Result.failure(workDataOf("Error" to it.toString())) ret = Result.failure(workDataOf("Error" to it.toString()))
} }
.blockingGet() .blockingGet()
.also { savedValues -> .also { result ->
savedValues.all().forEach { result.all().forEach {
xdripSourcePlugin.detectSource(it) xdripSourcePlugin.detectSource(it)
aapsLogger.debug(LTag.DATABASE, "Inserted bg $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) xdripSourcePlugin.sensorBatteryLevel = bundle.getInt(Intents.EXTRA_SENSOR_BATTERY, -1)
return ret return ret

View file

@ -1,25 +1,115 @@
package app.aaps.plugins.source 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.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 app.aaps.shared.tests.TestBase
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.HasAndroidInjector
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when`
class XdripSourcePluginTest : TestBase() { class XdripSourcePluginTest : TestBase() {
private lateinit var xdripSourcePlugin: XdripSourcePlugin 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 rh: ResourceHelper
@Mock lateinit var context: ContextWithInjector
@BeforeEach @BeforeEach
fun setup() { 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<Bundle, XdripSourcePlugin.XdripSourceWorker> {
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<XdripSourcePlugin.XdripSourceWorker>(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() { @Test fun advancedFilteringSupported() {
assertThat(xdripSourcePlugin.advancedFilteringSupported()).isFalse() 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()
}
} }

View file

@ -30,6 +30,7 @@ dependencies {
testImplementation(project(":shared:tests")) testImplementation(project(":shared:tests"))
testImplementation(project(":implementation")) testImplementation(project(":implementation"))
testImplementation(project(":plugins:aps")) testImplementation(project(":plugins:aps"))
androidTestImplementation(project(":shared:tests"))
// OpenHuman // OpenHuman
api(Libs.Squareup.Okhttp3.okhttp) api(Libs.Squareup.Okhttp3.okhttp)
@ -49,6 +50,10 @@ dependencies {
// DataLayerListenerService // DataLayerListenerService
api(Libs.Google.Android.PlayServices.wearable) api(Libs.Google.Android.PlayServices.wearable)
// Garmin
api(Libs.connectiqSdk)
androidTestImplementation(Libs.connectiqSdk)
kapt(Libs.Dagger.compiler) kapt(Libs.Dagger.compiler)
kapt(Libs.Dagger.androidProcessor) kapt(Libs.Dagger.androidProcessor)
} }

View file

@ -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<String, BroadcastReceiver>()
// Maps app ids to intent actions.
private val receivers = mutableMapOf<String, String>()
private val receiver = mock<GarminReceiver>()
private val binder = mock<IBinder>() {
on { isBinderAlive } doReturn true
}
private val ciqService = mock<IConnectIQService>() {
on { asBinder() } doReturn binder
on { connectedDevices } doReturn listOf(IQDevice(1L, "TDevice"))
on { registerApp(any(), any(), any()) }.doAnswer { i ->
receivers[i.getArgument<IQApp>(0).applicationId] = i.getArgument(1)
}
}
private val context = mock<Context>() {
on { packageName } doReturn this@GarminDeviceClientTest.packageName
on { registerReceiver(any<BroadcastReceiver>(), any()) } doAnswer { i ->
actions[i.getArgument<IntentFilter>(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<Executor>(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) })
}
}

View file

@ -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]"
}

View file

@ -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<GarminDevice>
/** 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)
}

View file

@ -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
}
}

View file

@ -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<String>()
private val broadcastReceiver = mutableListOf<BroadcastReceiver>()
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<GarminDevice>
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<Pair<Long, String>, Queue<Message>> ()
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
}
}

View file

@ -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<String, String>,
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<Long, GarminDevice>()
private val clients = mutableListOf<GarminClient>()
private val appIdNames = mutableMapOf<String, String>()
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
}

View file

@ -1,5 +1,6 @@
package app.aaps.plugins.sync.garmin package app.aaps.plugins.sync.garmin
import android.content.Context
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.db.GlucoseUnit
import app.aaps.core.interfaces.logging.AAPSLogger 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.core.interfaces.sharedPreferences.SP
import app.aaps.database.entities.GlucoseValue import app.aaps.database.entities.GlucoseValue
import app.aaps.plugins.sync.R import app.aaps.plugins.sync.R
import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers 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.SocketAddress
import java.net.URI import java.net.URI
import java.time.Clock import java.time.Clock
@ -42,6 +48,7 @@ class GarminPlugin @Inject constructor(
injector: HasAndroidInjector, injector: HasAndroidInjector,
aapsLogger: AAPSLogger, aapsLogger: AAPSLogger,
resourceHelper: ResourceHelper, resourceHelper: ResourceHelper,
private val context: Context,
private val loopHub: LoopHub, private val loopHub: LoopHub,
private val rxBus: RxBus, private val rxBus: RxBus,
private val sp: SP, private val sp: SP,
@ -57,7 +64,19 @@ class GarminPlugin @Inject constructor(
) { ) {
/** HTTP Server for local HTTP server communication (device app requests values) .*/ /** HTTP Server for local HTTP server communication (device app requests values) .*/
private var server: HttpServer? = null 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() private val disposable = CompositeDisposable()
@VisibleForTesting @VisibleForTesting
@ -68,10 +87,24 @@ class GarminPlugin @Inject constructor(
var newValue: Condition = valueLock.newCondition() var newValue: Condition = valueLock.newCondition()
private var lastGlucoseValueTimestamp: Long? = null private var lastGlucoseValueTimestamp: Long? = null
private val glucoseUnitStr get() = if (loopHub.glucoseUnit == GlucoseUnit.MGDL) "mgdl" else "mmoll" 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) { private fun onPreferenceChange(event: EventPreferenceChange) {
aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}") when (event.changedKey) {
setupHttpServer() "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() { override fun onStart() {
@ -83,19 +116,34 @@ class GarminPlugin @Inject constructor(
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.subscribe(::onPreferenceChange) .subscribe(::onPreferenceChange)
) )
disposable.add(
rxBus
.toObservable(EventNewBG::class.java)
.observeOn(Schedulers.io())
.subscribe(::onNewBloodGlucose)
)
setupHttpServer() setupHttpServer()
if (garminAapsKey.isNotEmpty())
setupGarminMessenger()
} }
private fun setupHttpServer() { private fun setupHttpServer() {
setupHttpServer(Duration.ZERO)
}
@VisibleForTesting
fun setupHttpServer(wait: Duration) {
if (sp.getBoolean("communication_http", false)) { if (sp.getBoolean("communication_http", false)) {
val port = sp.getInt("communication_http_port", 28891) val port = sp.getInt("communication_http_port", 28891)
if (server != null && server?.port == port) return if (server != null && server?.port == port) return
aapsLogger.info(LTag.GARMIN, "starting HTTP server on $port") aapsLogger.info(LTag.GARMIN, "starting HTTP server on $port")
server?.close() server?.close()
server = HttpServer(aapsLogger, port).apply { server = HttpServer(aapsLogger, port).apply {
registerEndpoint("/get", ::onGetBloodGlucose) registerEndpoint("/get", requestHandler(::onGetBloodGlucose))
registerEndpoint("/carbs", ::onPostCarbs) registerEndpoint("/carbs", requestHandler(::onPostCarbs))
registerEndpoint("/connect", ::onConnectPump) registerEndpoint("/connect", requestHandler(::onConnectPump))
registerEndpoint("/sgv.json", requestHandler(::onSgv))
awaitReady(wait)
} }
} else if (server != null) { } else if (server != null) {
aapsLogger.info(LTag.GARMIN, "stopping HTTP server") aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
@ -104,7 +152,7 @@ class GarminPlugin @Inject constructor(
} }
} }
override fun onStop() { public override fun onStop() {
disposable.clear() disposable.clear()
aapsLogger.info(LTag.GARMIN, "Stop") aapsLogger.info(LTag.GARMIN, "Stop")
server?.close() 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<String, Any>(
"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. */ /** Gets the last 2+ hours of glucose values. */
@VisibleForTesting @VisibleForTesting
fun getGlucoseValues(): List<GlucoseValue> { fun getGlucoseValues(): List<GlucoseValue> {
@ -161,21 +239,33 @@ class GarminPlugin @Inject constructor(
val glucoseMgDl: Int = glucose.value.roundToInt() val glucoseMgDl: Int = glucose.value.roundToInt()
encodedGlucose.add(timeSec, glucoseMgDl) encodedGlucose.add(timeSec, glucoseMgDl)
} }
aapsLogger.info(
LTag.GARMIN,
"retrieved ${glucoseValues.size} last ${Date(glucoseValues.lastOrNull()?.timestamp ?: 0L)} ${encodedGlucose.size}"
)
return encodedGlucose.encodedBase64() 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. /** Responses to get glucose value request by the device.
* *
* Also, gets the heart rate readings from the device. * Also, gets the heart rate readings from the device.
*/ */
@VisibleForTesting @VisibleForTesting
@Suppress("UNUSED_PARAMETER") fun onGetBloodGlucose(uri: URI): CharSequence {
fun onGetBloodGlucose(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri")
receiveHeartRate(uri) receiveHeartRate(uri)
val profileName = loopHub.currentProfileName val profileName = loopHub.currentProfileName
val waitSec = getQueryParameter(uri, "wait", 0L) val waitSec = getQueryParameter(uri, "wait", 0L)
@ -189,9 +279,7 @@ class GarminPlugin @Inject constructor(
} }
jo.addProperty("profile", profileName.first().toString()) jo.addProperty("profile", profileName.first().toString())
jo.addProperty("connected", loopHub.isConnected) jo.addProperty("connected", loopHub.isConnected)
return jo.toString().also { return jo.toString()
aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri, result: $it")
}
} }
private fun getQueryParameter(uri: URI, name: String) = (uri.query ?: "") 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<String, Any>, 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 @VisibleForTesting
fun receiveHeartRate(uri: URI) { fun receiveHeartRate(uri: URI) {
val avg: Int = getQueryParameter(uri, "hr", 0L).toInt() val avg: Int = getQueryParameter(uri, "hr", 0L).toInt()
@ -237,20 +338,18 @@ class GarminPlugin @Inject constructor(
private fun receiveHeartRate( private fun receiveHeartRate(
samplingStart: Instant, samplingEnd: Instant, samplingStart: Instant, samplingEnd: Instant,
avg: Int, device: String?, test: Boolean) { 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 (test) return
if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) { if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) {
loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device) loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device)
} else { } else if (avg > 0) {
aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd") aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd")
} }
} }
/** Handles carb notification from the device. */ /** Handles carb notification from the device. */
@VisibleForTesting @VisibleForTesting
@Suppress("UNUSED_PARAMETER") fun onPostCarbs(uri: URI): CharSequence {
fun onPostCarbs(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
aapsLogger.info(LTag.GARMIN, "carbs from $caller, req: $uri")
postCarbs(getQueryParameter(uri, "carbs", 0L).toInt()) postCarbs(getQueryParameter(uri, "carbs", 0L).toInt())
return "" return ""
} }
@ -263,9 +362,7 @@ class GarminPlugin @Inject constructor(
/** Handles pump connected notification that the user entered on the Garmin device. */ /** Handles pump connected notification that the user entered on the Garmin device. */
@VisibleForTesting @VisibleForTesting
@Suppress("UNUSED_PARAMETER") fun onConnectPump(uri: URI): CharSequence {
fun onConnectPump(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
aapsLogger.info(LTag.GARMIN, "connect from $caller, req: $uri")
val minutes = getQueryParameter(uri, "disconnectMinutes", 0L).toInt() val minutes = getQueryParameter(uri, "disconnectMinutes", 0L).toInt()
if (minutes > 0) { if (minutes > 0) {
loopHub.disconnectPump(minutes) loopHub.disconnectPump(minutes)
@ -277,4 +374,61 @@ class GarminPlugin @Inject constructor(
jo.addProperty("connected", loopHub.isConnected) jo.addProperty("connected", loopHub.isConnected)
return jo.toString() 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()
}
} }

View file

@ -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?)
}

View file

@ -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:
* <STRS_MARKER><STRS_LEN><STRINGS><OBJS_MARKER><OBJS_LENGTH><OBJ><OBJ>...
*
* 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<Int, String>, container: Queue<Container>)
}
private class ListContainer(
val size: Int,
val list: MutableList<Any?>
) : Container {
override fun read(buf: ByteBuffer, strings: Map<Int, String>, container: Queue<Container>) {
for (i in 0 until size) {
list.add(readObject(buf, strings, container))
}
}
}
private class MapContainer(
val size: Int,
val map: MutableMap<Any, Any?>
) : Container {
override fun read(buf: ByteBuffer, strings: Map<Int, String>, container: Queue<Container>) {
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<String, Int>()
val q = ArrayDeque<Any?>()
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<String, Int>,
q: Queue<Any?>
) {
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<Int, String> {
val strings = mutableMapOf<Int, String>()
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<Int, String>): Any? {
val objBufferLen = buf.int
if (objBufferLen > buf.remaining()) {
throw IllegalArgumentException("expect $objBufferLen bytes got ${buf.remaining()}")
}
val container = ArrayDeque<Container>()
val r = readObject(buf, strings, container)
while (container.isNotEmpty()) {
container.pollFirst()?.read(buf, strings, container)
}
return r
}
private fun readObject(buf: ByteBuffer, strings: Map<Int, String>, q: Queue<Container>): 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<Any?>()
// 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<Any, Any?>()
q.add(MapContainer(mapSize, map))
return map
}
LONG -> return buf.long
DOUBLE -> return buf.double
CHAR -> return Char(buf.int)
else -> return null
}
}
}

View file

@ -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<Connection> = 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<GarminDevice> 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
}

View file

@ -34,7 +34,7 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po
private val serverThread: Thread private val serverThread: Thread
private val workerExecutor: Executor = Executors.newCachedThreadPool() private val workerExecutor: Executor = Executors.newCachedThreadPool()
private val endpoints: MutableMap<String, (SocketAddress, URI, String?) -> CharSequence> = private val endpoints: MutableMap<String, (SocketAddress, URI, String?) -> Pair<Int, CharSequence>> =
ConcurrentHashMap() ConcurrentHashMap()
private var serverSocket: ServerSocket? = null private var serverSocket: ServerSocket? = null
private val readyLock = ReentrantLock() 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. */ /** 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<Int, CharSequence>) {
aapsLogger.info(LTag.GARMIN, "Register: '$path'") aapsLogger.info(LTag.GARMIN, "Register: '$path'")
endpoints[path] = endpoint endpoints[path] = endpoint
} }
@ -127,8 +127,8 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po
respond(HttpURLConnection.HTTP_NOT_FOUND, out) respond(HttpURLConnection.HTTP_NOT_FOUND, out)
} else { } else {
try { try {
val body = endpoint(s.remoteSocketAddress, uri, reqBody) val (code, body) = endpoint(s.remoteSocketAddress, uri, reqBody)
respond(HttpURLConnection.HTTP_OK, body, "application/json", out) respond(code, body, "application/json", out)
} catch (e: Exception) { } catch (e: Exception) {
aapsLogger.error(LTag.GARMIN, "endpoint " + uri.path + " failed", e) aapsLogger.error(LTag.GARMIN, "endpoint " + uri.path + " failed", e)
respond(HttpURLConnection.HTTP_INTERNAL_ERROR, out) respond(HttpURLConnection.HTTP_INTERNAL_ERROR, out)

View file

@ -20,6 +20,12 @@ interface LoopHub {
/** Returns the remaining bolus insulin on board. */ /** Returns the remaining bolus insulin on board. */
val insulinOnboard: Double 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. */ /** Returns true if the pump is connected. */
val isConnected: Boolean val isConnected: Boolean

View file

@ -13,6 +13,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction
import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.pump.DetailedBolusInfo
import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.queue.CommandQueue
import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.core.main.graph.OverviewData
import app.aaps.database.ValueWrapper import app.aaps.database.ValueWrapper
import app.aaps.database.entities.EffectiveProfileSwitch import app.aaps.database.entities.EffectiveProfileSwitch
import app.aaps.database.entities.GlucoseValue import app.aaps.database.entities.GlucoseValue
@ -42,6 +43,7 @@ class LoopHubImpl @Inject constructor(
private val repo: AppRepository, private val repo: AppRepository,
private val userEntryLogger: UserEntryLogger, private val userEntryLogger: UserEntryLogger,
private val sp: SP, private val sp: SP,
private val overviewData: OverviewData,
) : LoopHub { ) : LoopHub {
@VisibleForTesting @VisibleForTesting
@ -64,6 +66,14 @@ class LoopHubImpl @Inject constructor(
override val insulinOnboard: Double override val insulinOnboard: Double
get() = iobCobCalculator.calculateIobFromBolus().iob 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. */ /** Returns true if the pump is connected. */
override val isConnected: Boolean get() = !loop.isDisconnected override val isConnected: Boolean get() = !loop.isDisconnected

View file

@ -167,7 +167,8 @@ class NSClientV3Service : DaggerService() {
rxBus.send(EventNSClientNewLog("◄ WS", "Subscribed for: ${response.optString("collections")}")) rxBus.send(EventNSClientNewLog("◄ WS", "Subscribed for: ${response.optString("collections")}"))
// during disconnection updated data is not received // during disconnection updated data is not received
// thus run non WS load to get missing data // 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 true
} else { } else {
rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed")) rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed"))

View file

@ -1,4 +1,4 @@
package app.aaps.plugins.sync.dataBroadcaster package app.aaps.plugins.sync.tizen
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -41,7 +41,7 @@ import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class DataBroadcastPlugin @Inject constructor( class TizenPlugin @Inject constructor(
injector: HasAndroidInjector, injector: HasAndroidInjector,
aapsLogger: AAPSLogger, aapsLogger: AAPSLogger,
rh: ResourceHelper, rh: ResourceHelper,
@ -64,9 +64,9 @@ class DataBroadcastPlugin @Inject constructor(
PluginDescription() PluginDescription()
.mainType(PluginType.SYNC) .mainType(PluginType.SYNC)
.pluginIcon(app.aaps.core.main.R.drawable.ic_watch) .pluginIcon(app.aaps.core.main.R.drawable.ic_watch)
.pluginName(R.string.data_broadcaster) .pluginName(R.string.tizen)
.shortName(R.string.data_broadcaster_short) .shortName(R.string.tizen_short)
.description(R.string.data_broadcaster_description), .description(R.string.tizen_description),
aapsLogger, rh, injector aapsLogger, rh, injector
) { ) {

View file

@ -117,8 +117,9 @@
<string name="xdrip_local_broadcasts_summary">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í</string> <string name="xdrip_local_broadcasts_summary">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í</string>
<string name="xdrip_local_broadcasts_title">Povolit odesílání do xDrip+.</string> <string name="xdrip_local_broadcasts_title">Povolit odesílání do xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string> <string name="tizen">Samsung Tizen</string>
<string name="data_broadcaster_description">Odesílání dat do Garmin aplikace G-Watch Wear App</string> <string name="tizen_short">TIZ</string>
<string name="tizen_description">Odesílání dat do Samsung aplikace G-Watch Wear App (Tizen OS)</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Připojení k zařízení Garmin (Fenix, Edge, …)</string> <string name="garmin_description">Připojení k zařízení Garmin (Fenix, Edge, …)</string>

View file

@ -117,8 +117,6 @@
<string name="xdrip_local_broadcasts_summary">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</string> <string name="xdrip_local_broadcasts_summary">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</string>
<string name="xdrip_local_broadcasts_title">Activar las transmisiones a xDrip+</string> <string name="xdrip_local_broadcasts_title">Activar las transmisiones a xDrip+</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string>
<string name="data_broadcaster_description">Transmitir datos a la aplicación G-Watch Wear de Garmin</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Conexión al dispositivo Garmin (Fénix, Edge, …)</string> <string name="garmin_description">Conexión al dispositivo Garmin (Fénix, Edge, …)</string>
@ -128,7 +126,7 @@
<string name="description_wear">Supervisar y controlar AAPS usando un reloj WearOS</string> <string name="description_wear">Supervisar y controlar AAPS usando un reloj WearOS</string>
<string name="no_watch_connected">(Ningún reloj conectado)</string> <string name="no_watch_connected">(Ningún reloj conectado)</string>
<string name="pump_status">Estado de la bomba de insulina</string> <string name="pump_status">Estado de la bomba de insulina</string>
<string name="loop_status">Estado del lazo</string> <string name="loop_status">Estado del bucle</string>
<string name="wizard_result">Calc. Asistente:\nInsulina: %1$.2fU\nCarbohidratos: %2$dg</string> <string name="wizard_result">Calc. Asistente:\nInsulina: %1$.2fU\nCarbohidratos: %2$dg</string>
<string name="quick_wizard_not_available">El asistente rápido seleccionado ya no está disponible, por favor actualice su tarjeta</string> <string name="quick_wizard_not_available">El asistente rápido seleccionado ya no está disponible, por favor actualice su tarjeta</string>
<string name="quick_wizard_message">Asistente Rápido: %1$s\nInsulina: %2$.2fU\nCarbohidratos: %3$dg</string> <string name="quick_wizard_message">Asistente Rápido: %1$s\nInsulina: %2$.2fU\nCarbohidratos: %3$dg</string>

View file

@ -17,6 +17,7 @@
<string name="description_ns_client_v3">Synchronise vos données avec Nightscout en utilisant l\'API v3</string> <string name="description_ns_client_v3">Synchronise vos données avec Nightscout en utilisant l\'API v3</string>
<string name="blocked_by_charging">Bloqué par les options de recharge</string> <string name="blocked_by_charging">Bloqué par les options de recharge</string>
<string name="blocked_by_connectivity">Bloqué par les options de connectivité</string> <string name="blocked_by_connectivity">Bloqué par les options de connectivité</string>
<string name="no_connectivity">Aucune connectivité</string>
<string name="unsupported_ns_version">Version de Nightscout non supportée</string> <string name="unsupported_ns_version">Version de Nightscout non supportée</string>
<string name="openaps_short">OAPS</string> <string name="openaps_short">OAPS</string>
<string name="uploader_short">UPLD</string> <string name="uploader_short">UPLD</string>
@ -116,9 +117,6 @@
<string name="xdrip_local_broadcasts_summary">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</string> <string name="xdrip_local_broadcasts_summary">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</string>
<string name="xdrip_local_broadcasts_title">Activer les diffusions vers xDrip+.</string> <string name="xdrip_local_broadcasts_title">Activer les diffusions vers xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster">Diffuseur de données</string>
<string name="data_broadcaster_short">DD</string>
<string name="data_broadcaster_description">Diffuser des données sur l\'application G-Watch Wear de Garmin</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Connexion au périphérique Garmin (Fenix, Edge, …)</string> <string name="garmin_description">Connexion au périphérique Garmin (Fenix, Edge, …)</string>

View file

@ -117,8 +117,9 @@
<string name="xdrip_local_broadcasts_summary">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</string> <string name="xdrip_local_broadcasts_summary">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</string>
<string name="xdrip_local_broadcasts_title">Abilita trasmissioni a xDrip+.</string> <string name="xdrip_local_broadcasts_title">Abilita trasmissioni a xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string> <string name="tizen">Samsung Tizen</string>
<string name="data_broadcaster_description">Trasmetti i dati all\'app G-Watch Wear di Garmin</string> <string name="tizen_short">TIZ</string>
<string name="tizen_description">Trasmetti i dati all\'app G-Watch Wear di Samsung (Tizen OS)</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Connessione al dispositivo Garmin (Fenix, Edge, …)</string> <string name="garmin_description">Connessione al dispositivo Garmin (Fenix, Edge, …)</string>

View file

@ -117,7 +117,6 @@
<string name="xdrip_local_broadcasts_summary">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ą</string> <string name="xdrip_local_broadcasts_summary">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ą</string>
<string name="xdrip_local_broadcasts_title">Įjungti duomenų perdavimą į xDrip+.</string> <string name="xdrip_local_broadcasts_title">Įjungti duomenų perdavimą į xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster">Data Broadcaster</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Susiejimas su Garmin (Fenix, Edge, …)</string> <string name="garmin_description">Susiejimas su Garmin (Fenix, Edge, …)</string>

View file

@ -117,8 +117,9 @@
<string name="xdrip_local_broadcasts_summary">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</string> <string name="xdrip_local_broadcasts_summary">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</string>
<string name="xdrip_local_broadcasts_title">Aktiver sending til xDrip+.</string> <string name="xdrip_local_broadcasts_title">Aktiver sending til xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string> <string name="tizen">Samsung Tizen</string>
<string name="data_broadcaster_description">Kringkast data til Garmin\'s G-Watch Wear app</string> <string name="tizen_short">TIZ</string>
<string name="tizen_description">Kringkast data til Samsung\'s G-Watch Wear app (Tizen OS)</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Tilkobling til Garmin-enheter (Fenix, Edge,…)</string> <string name="garmin_description">Tilkobling til Garmin-enheter (Fenix, Edge,…)</string>

View file

@ -117,8 +117,6 @@
<string name="xdrip_local_broadcasts_summary">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</string> <string name="xdrip_local_broadcasts_summary">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</string>
<string name="xdrip_local_broadcasts_title">Activeer uitzendingen naar xDrip+.</string> <string name="xdrip_local_broadcasts_title">Activeer uitzendingen naar xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string>
<string name="data_broadcaster_description">Verzend gegevens naar Garmin\'s G-Watch Wear App</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Verbinding met Garmin apparaat (Fenix, Edge, …)</string> <string name="garmin_description">Verbinding met Garmin apparaat (Fenix, Edge, …)</string>

View file

@ -117,8 +117,6 @@
<string name="xdrip_local_broadcasts_summary">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\"</string> <string name="xdrip_local_broadcasts_summary">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\"</string>
<string name="xdrip_local_broadcasts_title">Włącz nadawanie do xDrip+.</string> <string name="xdrip_local_broadcasts_title">Włącz nadawanie do xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string>
<string name="data_broadcaster_description">Transmisja danych do aplikacji G-Watch Garmin</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Połączenie z urządzeniem Garmin (Fenix, Edge, …)</string> <string name="garmin_description">Połączenie z urządzeniem Garmin (Fenix, Edge, …)</string>

View file

@ -117,8 +117,9 @@
<string name="xdrip_local_broadcasts_summary">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”</string> <string name="xdrip_local_broadcasts_summary">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”</string>
<string name="xdrip_local_broadcasts_title">Activează transmisiuni spre xDrip+.</string> <string name="xdrip_local_broadcasts_title">Activează transmisiuni spre xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string> <string name="tizen">Samsung Tizen</string>
<string name="data_broadcaster_description">Transmitere date către aplicaţia G-Watch Wear pe Garmin</string> <string name="tizen_short">TIZ</string>
<string name="tizen_description">Transmitere de date către aplicaţia Samsung G-Watch Wear (TizenOS)</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Conexiune la dispozitivul Garmin (Fenix, Edge, …)</string> <string name="garmin_description">Conexiune la dispozitivul Garmin (Fenix, Edge, …)</string>

View file

@ -22,7 +22,7 @@
<string name="terms_of_use">Условия пользования</string> <string name="terms_of_use">Условия пользования</string>
<string name="please_read__information">Внимательно ознакомьтесь со следующей информацией и примите условия использования.</string> <string name="please_read__information">Внимательно ознакомьтесь со следующей информацией и примите условия использования.</string>
<string name="info_openhumans">Это проект с открытым исходным кодом, который будет копировать ваши данные на Open Humans. Мы не будем обмениваться вашими данными с третьими лицами без вашего явного разрешения. Данные, отправляемые на проект и приложение, идентифицируются с помощью случайного идентификатора и будут безопасно передаваться на учетную запись Open Humans при вашем одобрении этого процесса. Вы можете остановить загрузку и удалить загруженные данные в любое время здесь: www.openhumans.org.</string> <string name="info_openhumans">Это проект с открытым исходным кодом, который будет копировать ваши данные на Open Humans. Мы не будем обмениваться вашими данными с третьими лицами без вашего явного разрешения. Данные, отправляемые на проект и приложение, идентифицируются с помощью случайного идентификатора и будут безопасно передаваться на учетную запись Open Humans при вашем одобрении этого процесса. Вы можете остановить загрузку и удалить загруженные данные в любое время здесь: www.openhumans.org.</string>
<string name="data_uploaded">Данные загружены</string> <string name="data_uploaded">Загружаемые данные</string>
<string name="glucose_values">Гликемия</string> <string name="glucose_values">Гликемия</string>
<string name="boluses">Болюсы</string> <string name="boluses">Болюсы</string>
<string name="extended_boluses">Пролонгированные болюсы</string> <string name="extended_boluses">Пролонгированные болюсы</string>
@ -37,7 +37,7 @@
<string name="device_model">Модель устройства</string> <string name="device_model">Модель устройства</string>
<string name="screen_dimensions">Размеры экрана</string> <string name="screen_dimensions">Размеры экрана</string>
<string name="algorithm_debug_data">Данные отладки алгоритма</string> <string name="algorithm_debug_data">Данные отладки алгоритма</string>
<string name="data_not_uploaded">Данные НЕ загружены</string> <string name="data_not_uploaded">НЕзагружаемые данные</string>
<string name="passwords">Пароли</string> <string name="passwords">Пароли</string>
<string name="nightscout_url">URL-адрес Nightscout</string> <string name="nightscout_url">URL-адрес Nightscout</string>
<string name="nightscout_api_secret">Секретный ключ Nightscout API</string> <string name="nightscout_api_secret">Секретный ключ Nightscout API</string>

View file

@ -117,8 +117,9 @@
<string name="xdrip_local_broadcasts_summary">Отправлять данные о глюкозе и терапии на xDrip+. Источником данных должен быть выбран \"xDrip+ Sync Follower\" а в настройках между приложениями надо включить - Принимать глюкозу/терапию</string> <string name="xdrip_local_broadcasts_summary">Отправлять данные о глюкозе и терапии на xDrip+. Источником данных должен быть выбран \"xDrip+ Sync Follower\" а в настройках между приложениями надо включить - Принимать глюкозу/терапию</string>
<string name="xdrip_local_broadcasts_title">Включить трансляции для xDrip+.</string> <string name="xdrip_local_broadcasts_title">Включить трансляции для xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string> <string name="tizen">Samsung Tizen</string>
<string name="data_broadcaster_description">Передавать данные приложению Garmin\'s G-Watch Wear</string> <string name="tizen_short">TIZ</string>
<string name="tizen_description">Передавать данные приложению Samsung G-Watch Wear (OS Tizen)</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Подключение к устройству Garmin (Fenix, Edge, …)</string> <string name="garmin_description">Подключение к устройству Garmin (Fenix, Edge, …)</string>

View file

@ -17,6 +17,7 @@
<string name="description_ns_client_v3">Synchronizuje vaše dáta s Nightscoutom pomocou v3 API</string> <string name="description_ns_client_v3">Synchronizuje vaše dáta s Nightscoutom pomocou v3 API</string>
<string name="blocked_by_charging">Zablokované možnosti nabíjania</string> <string name="blocked_by_charging">Zablokované možnosti nabíjania</string>
<string name="blocked_by_connectivity">Zablokované možnosti pripojenia</string> <string name="blocked_by_connectivity">Zablokované možnosti pripojenia</string>
<string name="no_connectivity">Žiadne pripojenie</string>
<string name="unsupported_ns_version">Nepodporovaná verzia Nighscoutu</string> <string name="unsupported_ns_version">Nepodporovaná verzia Nighscoutu</string>
<string name="openaps_short">OAPS</string> <string name="openaps_short">OAPS</string>
<string name="uploader_short">UPLD</string> <string name="uploader_short">UPLD</string>
@ -116,8 +117,9 @@
<string name="xdrip_local_broadcasts_summary">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</string> <string name="xdrip_local_broadcasts_summary">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</string>
<string name="xdrip_local_broadcasts_title">Povoliť odosielanie do xDrip+.</string> <string name="xdrip_local_broadcasts_title">Povoliť odosielanie do xDrip+.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string> <string name="tizen">Samsung Tizen</string>
<string name="data_broadcaster_description">Odosielanie dát do Garmin aplikácie G-Watch Wear</string> <string name="tizen_short">TIZ</string>
<string name="tizen_description">Odosielanie dát do Samsung aplikácie G-Watch Wear App (Tizen OS)</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Pripájanie k zariadeniu Garmin (Fénix, Edge, ...)</string> <string name="garmin_description">Pripájanie k zariadeniu Garmin (Fénix, Edge, ...)</string>

View file

@ -117,8 +117,9 @@
<string name="xdrip_local_broadcasts_summary">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.</string> <string name="xdrip_local_broadcasts_summary">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.</string>
<string name="xdrip_local_broadcasts_title">xDrip+ \'a yayınları etkinleştirin.</string> <string name="xdrip_local_broadcasts_title">xDrip+ \'a yayınları etkinleştirin.</string>
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster_short">DBRO</string> <string name="tizen">Samsung Tizen</string>
<string name="data_broadcaster_description">Verileri Garmin\'in G-Watch Wear Uygulamasına yayınlayın</string> <string name="tizen_short">TIZ</string>
<string name="tizen_description">Verileri Samsung\'un G-Watch Wear Uygulamasına (Tizen OS) yayınlayın</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>
<string name="garmin_description">Garmin cihazına bağlantı (Fenix, Edge,…)</string> <string name="garmin_description">Garmin cihazına bağlantı (Fenix, Edge,…)</string>

View file

@ -180,9 +180,9 @@
<!-- DataBroadcast--> <!-- DataBroadcast-->
<string name="data_broadcaster">Data Broadcaster</string> <string name="tizen">Samsung Tizen</string>
<string name="data_broadcaster_short">DBRO</string> <string name="tizen_short">TIZ</string>
<string name="data_broadcaster_description">Broadcast data to Garmin\'s G-Watch Wear App</string> <string name="tizen_description">Broadcast data to Samsung\'s G-Watch Wear App (Tizen OS)</string>
<!-- GarminPlugin --> <!-- GarminPlugin -->
<string name="garmin">Garmin</string> <string name="garmin">Garmin</string>

View file

@ -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<Context>()
private var appId1 = "appId1"
private val appId2 = "appId2"
private val apps = mapOf(appId1 to "$appId1-name", appId2 to "$appId2-name")
private val outMessages = mutableListOf<Pair<GarminApplication, ByteArray>>()
private val inMessages = mutableListOf<Pair<GarminApplication, Any>>()
private var messenger = GarminMessenger(
aapsLogger, context, apps, { app, msg -> inMessages.add(app to msg) },
enableConnectIq = false, enableSimulator = false)
private val client1 = mock<GarminClient>() {
on { name } doReturn "Mock1"
on { sendMessage(any(), any()) } doAnswer { a ->
outMessages.add(a.getArgument<GarminApplication>(0) to a.getArgument(1))
Unit
}
}
private val client2 = mock<GarminClient>() {
on { name } doReturn "Mock2"
on { sendMessage(any(), any()) } doAnswer { a ->
outMessages.add(a.getArgument<GarminApplication>(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)
}
}

View file

@ -1,5 +1,6 @@
package app.aaps.plugins.sync.garmin package app.aaps.plugins.sync.garmin
import android.content.Context
import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.db.GlucoseUnit
import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.resources.ResourceHelper
import app.aaps.core.interfaces.rx.events.EventNewBG 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.AfterEach
import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals 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.BeforeEach
import org.junit.jupiter.api.Test 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.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.atMost import org.mockito.Mockito.atMost
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
@ -21,19 +27,28 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` 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.SocketAddress
import java.net.URI import java.net.URI
import java.time.Clock import java.time.Clock
import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.concurrent.locks.Condition import java.util.concurrent.locks.Condition
import kotlin.ranges.LongProgression.Companion.fromClosedRange
class GarminPluginTest: TestBase() { class GarminPluginTest: TestBase() {
private lateinit var gp: GarminPlugin private lateinit var gp: GarminPlugin
@Mock private lateinit var rh: ResourceHelper @Mock private lateinit var rh: ResourceHelper
@Mock private lateinit var sp: SP @Mock private lateinit var sp: SP
@Mock private lateinit var context: Context
@Mock private lateinit var loopHub: LoopHub @Mock private lateinit var loopHub: LoopHub
private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC"))
@ -44,14 +59,23 @@ class GarminPluginTest: TestBase() {
@BeforeEach @BeforeEach
fun setup() { fun setup() {
gp = GarminPlugin(injector, aapsLogger, rh, loopHub, rxBus, sp) gp = GarminPlugin(injector, aapsLogger, rh, context, loopHub, rxBus, sp)
gp.clock = clock gp.clock = clock
`when`(loopHub.currentProfileName).thenReturn("Default") `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 @AfterEach
fun verifyNoFurtherInteractions() { fun verifyNoFurtherInteractions() {
verify(loopHub, atMost(2)).currentProfileName 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) verifyNoMoreInteractions(loopHub)
} }
@ -71,11 +95,23 @@ class GarminPluginTest: TestBase() {
"device" to "Test_Device") "device" to "Test_Device")
private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue( private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue(
id = 10 * timestamp.toEpochMilli(),
timestamp = timestamp.toEpochMilli(), raw = 90.0, value = value, 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 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 @Test
fun testReceiveHeartRateUri() { fun testReceiveHeartRateUri() {
val hr = createHeartRate(99) val hr = createHeartRate(99)
@ -119,6 +155,132 @@ class GarminPluginTest: TestBase() {
verify(loopHub).getGlucoseValues(from, true) 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<String, Any>
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<String, Any>
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 @Test
fun testOnGetBloodGlucose() { fun testOnGetBloodGlucose() {
`when`(loopHub.isConnected).thenReturn(true) `when`(loopHub.isConnected).thenReturn(true)
@ -129,7 +291,7 @@ class GarminPluginTest: TestBase() {
listOf(createGlucoseValue(Instant.ofEpochSecond(1_000)))) listOf(createGlucoseValue(Instant.ofEpochSecond(1_000))))
val hr = createHeartRate(99) val hr = createHeartRate(99)
val uri = createUri(hr) val uri = createUri(hr)
val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null) val result = gp.onGetBloodGlucose(uri)
assertEquals( assertEquals(
"{\"encodedGlucose\":\"0A+6AQ==\"," + "{\"encodedGlucose\":\"0A+6AQ==\"," +
"\"remainingInsulin\":3.14," + "\"remainingInsulin\":3.14," +
@ -161,7 +323,7 @@ class GarminPluginTest: TestBase() {
params["wait"] = 10 params["wait"] = 10
val uri = createUri(params) val uri = createUri(params)
gp.newValue = mock(Condition::class.java) gp.newValue = mock(Condition::class.java)
val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null) val result = gp.onGetBloodGlucose(uri)
assertEquals( assertEquals(
"{\"encodedGlucose\":\"/wS6AQ==\"," + "{\"encodedGlucose\":\"/wS6AQ==\"," +
"\"remainingInsulin\":3.14," + "\"remainingInsulin\":3.14," +
@ -184,7 +346,7 @@ class GarminPluginTest: TestBase() {
@Test @Test
fun testOnPostCarbs() { fun testOnPostCarbs() {
val uri = createUri(mapOf("carbs" to "12")) val uri = createUri(mapOf("carbs" to "12"))
assertEquals("", gp.onPostCarbs(mock(SocketAddress::class.java), uri, null)) assertEquals("", gp.onPostCarbs(uri))
verify(loopHub).postCarbs(12) verify(loopHub).postCarbs(12)
} }
@ -192,7 +354,7 @@ class GarminPluginTest: TestBase() {
fun testOnConnectPump_Disconnect() { fun testOnConnectPump_Disconnect() {
val uri = createUri(mapOf("disconnectMinutes" to "20")) val uri = createUri(mapOf("disconnectMinutes" to "20"))
`when`(loopHub.isConnected).thenReturn(false) `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).disconnectPump(20)
verify(loopHub).isConnected verify(loopHub).isConnected
} }
@ -201,8 +363,69 @@ class GarminPluginTest: TestBase() {
fun testOnConnectPump_Connect() { fun testOnConnectPump_Connect() {
val uri = createUri(mapOf("disconnectMinutes" to "0")) val uri = createUri(mapOf("disconnectMinutes" to "0"))
`when`(loopHub.isConnected).thenReturn(true) `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).connectPump()
verify(loopHub).isConnected 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<Instant>(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
}
} }

View file

@ -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))
}
}

View file

@ -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 <T> 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())
}
}

View file

@ -77,7 +77,7 @@ internal class HttpServerTest: TestBase() {
HttpServer(aapsLogger, port).use { server -> HttpServer(aapsLogger, port).use { server ->
server.registerEndpoint("/foo") { _: SocketAddress, uri: URI, _: String? -> server.registerEndpoint("/foo") { _: SocketAddress, uri: URI, _: String? ->
assertEquals(URI("/foo"), uri) assertEquals(URI("/foo"), uri)
"test" HttpURLConnection.HTTP_OK to "test"
} }
assertTrue(server.awaitReady(Duration.ofSeconds(10))) assertTrue(server.awaitReady(Duration.ofSeconds(10)))
val resp = reqUri.toURL().openConnection() as HttpURLConnection val resp = reqUri.toURL().openConnection() as HttpURLConnection

View file

@ -5,6 +5,7 @@ import app.aaps.core.interfaces.aps.Loop
import app.aaps.core.interfaces.constraints.Constraint import app.aaps.core.interfaces.constraints.Constraint
import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.constraints.ConstraintsChecker
import app.aaps.core.interfaces.db.GlucoseUnit 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.IobCobCalculator
import app.aaps.core.interfaces.iob.IobTotal import app.aaps.core.interfaces.iob.IobTotal
import app.aaps.core.interfaces.logging.UserEntryLogger 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.pump.DetailedBolusInfo
import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.queue.CommandQueue
import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.core.main.graph.OverviewData
import app.aaps.database.ValueWrapper import app.aaps.database.ValueWrapper
import app.aaps.database.entities.EffectiveProfileSwitch import app.aaps.database.entities.EffectiveProfileSwitch
import app.aaps.database.entities.GlucoseValue import app.aaps.database.entities.GlucoseValue
@ -54,6 +56,7 @@ class LoopHubTest: TestBase() {
@Mock lateinit var repo: AppRepository @Mock lateinit var repo: AppRepository
@Mock lateinit var userEntryLogger: UserEntryLogger @Mock lateinit var userEntryLogger: UserEntryLogger
@Mock lateinit var sp: SP @Mock lateinit var sp: SP
@Mock lateinit var overviewData: OverviewData
private lateinit var loopHub: LoopHubImpl private lateinit var loopHub: LoopHubImpl
private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC"))
@ -62,7 +65,7 @@ class LoopHubTest: TestBase() {
fun setup() { fun setup() {
loopHub = LoopHubImpl( loopHub = LoopHubImpl(
aapsLogger, commandQueue, constraints, iobCobCalculator, loop, aapsLogger, commandQueue, constraints, iobCobCalculator, loop,
profileFunction, repo, userEntryLogger, sp profileFunction, repo, userEntryLogger, sp, overviewData
) )
loopHub.clock = clock loopHub.clock = clock
} }
@ -76,9 +79,10 @@ class LoopHubTest: TestBase() {
verifyNoMoreInteractions(profileFunction) verifyNoMoreInteractions(profileFunction)
verifyNoMoreInteractions(repo) verifyNoMoreInteractions(repo)
verifyNoMoreInteractions(userEntryLogger) verifyNoMoreInteractions(userEntryLogger)
verifyNoMoreInteractions(overviewData)
} }
@Test @Test
fun testCurrentProfile() { fun testCurrentProfile() {
val profile = mock(Profile::class.java) val profile = mock(Profile::class.java)
`when`(profileFunction.getProfile()).thenReturn(profile) `when`(profileFunction.getProfile()).thenReturn(profile)
@ -109,6 +113,22 @@ class LoopHubTest: TestBase() {
verify(iobCobCalculator, times(1)).calculateIobFromBolus() 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 @Test
fun testIsConnected() { fun testIsConnected() {
`when`(loop.isDisconnected).thenReturn(false) `when`(loop.isDisconnected).thenReturn(false)

View file

@ -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.AutosensDataStore
import app.aaps.core.interfaces.aps.Loop import app.aaps.core.interfaces.aps.Loop
@ -27,7 +27,7 @@ import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
internal class DataBroadcastPluginTest : TestBaseWithProfile() { internal class TizenPluginTest : TestBaseWithProfile() {
@Mock lateinit var defaultValueHelper: DefaultValueHelper @Mock lateinit var defaultValueHelper: DefaultValueHelper
@Mock lateinit var loop: Loop @Mock lateinit var loop: Loop
@ -36,13 +36,13 @@ internal class DataBroadcastPluginTest : TestBaseWithProfile() {
@Mock lateinit var autosensDataStore: AutosensDataStore @Mock lateinit var autosensDataStore: AutosensDataStore
@Mock lateinit var processedDeviceStatusData: ProcessedDeviceStatusData @Mock lateinit var processedDeviceStatusData: ProcessedDeviceStatusData
private lateinit var sut: DataBroadcastPlugin private lateinit var sut: TizenPlugin
private val injector = HasAndroidInjector { AndroidInjector { } } private val injector = HasAndroidInjector { AndroidInjector { } }
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
sut = DataBroadcastPlugin( sut = TizenPlugin(
injector, aapsLogger, rh, aapsSchedulers, context, dateUtil, fabricPrivacy, rxBus, iobCobCalculator, profileFunction, defaultValueHelper, processedDeviceStatusData, injector, aapsLogger, rh, aapsSchedulers, context, dateUtil, fabricPrivacy, rxBus, iobCobCalculator, profileFunction, defaultValueHelper, processedDeviceStatusData,
loop, activePlugin, receiverStatusStore, config, glucoseStatusProvider, decimalFormatter loop, activePlugin, receiverStatusStore, config, glucoseStatusProvider, decimalFormatter
) )

View file

@ -28,23 +28,23 @@
<string name="combo_reservoir_normal">Нормальный</string> <string name="combo_reservoir_normal">Нормальный</string>
<string name="combo_notification_check_time_date">Необходимо обновить часы помпы</string> <string name="combo_notification_check_time_date">Необходимо обновить часы помпы</string>
<string name="combo_pump_tbr_cancelled_warrning">Предупреждение об отмене скорости временного базала подтверждено</string> <string name="combo_pump_tbr_cancelled_warrning">Предупреждение об отмене скорости временного базала подтверждено</string>
<string name="combo_error_no_connection_no_bolus_delivered">Не удалось подключиться к помпе. Болюс не подан</string> <string name="combo_error_no_connection_no_bolus_delivered">Не удалось подключиться к помпе. Болюс не введен</string>
<string name="combo_error_no_bolus_delivered">Подача болюса не состоялась. Чтобы удостовериться, проверьте помпу во избежание двойного болюса и повторите подачу. Для защиты от ложных срабатываний болюсы не повторяются автоматически.</string> <string name="combo_error_no_bolus_delivered">Болюс не введен. Чтобы удостовериться, проверьте помпу во избежание двойного болюса и повторите подачу. Для защиты от ложных срабатываний болюсы не повторяются автоматически.</string>
<string name="combo_error_partial_bolus_delivered">Подано только %1$.2f ед. из запрошенного болюса %2$.2f ед. из-за ошибки. Пожалуйста, проверьте помпу, чтобы удостовериться в этом и принять соответствующие меры.</string> <string name="combo_error_partial_bolus_delivered">Введено только %1$.2f ед. из запрошенного болюса %2$.2f ед. из-за ошибки. Пожалуйста, проверьте помпу, чтобы удостовериться в этом и принять соответствующие меры.</string>
<string name="combo_error_bolus_verification_failed">Подача болюса и проверка истории помпы не состоялись, пожалуйста проверьте помпу. Если болюс был подан, он будет добавлен в назначения во время следующего соединения с помпой.</string> <string name="combo_error_bolus_verification_failed">Ввод болюса и проверка истории помпы не состоялись, пожалуйста проверьте помпу. Если болюс был введен, он будет добавлен в назначения во время следующего соединения с помпой.</string>
<string name="combo_reservoir_level_insufficient_for_bolus">В резервуаре недостаточно инсулина для болюса</string> <string name="combo_reservoir_level_insufficient_for_bolus">В резервуаре недостаточно инсулина для болюса</string>
<string name="combo_invalid_setup">Недопустимые установки помпы, проверьте документацию и убедитесь, что меню Quick Info называется QUICK INFO, используя приложение 360 для конфигурации помпы.</string> <string name="combo_invalid_setup">Недопустимые установки помпы, проверьте документацию и убедитесь, что меню Quick Info называется QUICK INFO, используя приложение 360 для конфигурации помпы.</string>
<string name="combo_actvity_reading_basal_profile">Чтение базального профиля</string> <string name="combo_actvity_reading_basal_profile">Чтение базального профиля</string>
<string name="combo_bolus_rejected_due_to_pump_history_change">История событий помпы изменилась с момента вычисления болюса. Болюс не подан. Пожалуйста пересчитайте потребность в болюсе.</string> <string name="combo_bolus_rejected_due_to_pump_history_change">История событий помпы изменилась с момента вычисления болюса. Болюс не введен. Пожалуйста пересчитайте потребность в болюсе.</string>
<string name="combo_error_updating_treatment_record">Болюс подан успешно, но запись о назначении не сделана. Это может быть вызвано тем, что за последние две минуты назначено два болюса одного объема. Пожалуйста проверьте историю событий помпы и записи о назначениях и добавьте пропущенные записи из Портала назначений. Не добавляйте записи с одним и тем же временем и одинаковым объемом.</string> <string name="combo_error_updating_treatment_record">Болюс введен успешно, но запись о назначении не сделана. Это может быть вызвано тем, что за последние две минуты назначено два болюса одного объема. Пожалуйста проверьте историю событий помпы и записи о назначениях и добавьте пропущенные записи из Портала назначений. Не добавляйте записи с одним и тем же временем и одинаковым объемом.</string>
<string name="combo_high_temp_rejected_due_to_pump_history_changes">Временная верхняя цель отклонена т. к. калькуляция не учитывала недавние изменения в истории событий помпы</string> <string name="combo_high_temp_rejected_due_to_pump_history_changes">Временная верхняя цель отклонена т. к. калькуляция не учитывала недавние изменения в истории событий помпы</string>
<string name="combo_activity_checking_pump_state">Обновление статуса помпы</string> <string name="combo_activity_checking_pump_state">Обновление статуса помпы</string>
<string name="combo_warning_pump_basal_rate_changed">Скорость базала на помпе изменилась и вскоре будет обновлена</string> <string name="combo_warning_pump_basal_rate_changed">Скорость базала на помпе изменилась и вскоре будет обновлена</string>
<string name="combo_error_failure_reading_changed_basal_rate">Скорость базала на помпе изменилась, но учесть ее не удалось</string> <string name="combo_error_failure_reading_changed_basal_rate">Скорость базала на помпе изменилась, но учесть ее не удалось</string>
<string name="combo_activity_checking_for_history_changes">Проверка изменений в истории событий помпы</string> <string name="combo_activity_checking_for_history_changes">Проверка изменений в истории событий помпы</string>
<string name="combo_error_multiple_boluses_with_identical_timestamp">Только что импортировано несколько болюсов с одинаковым количеством инсулина в течение одной минуты. В лог лечения может быть добавлена только одна запись. Пожалуйста проверьте помпу и вручную введите запись о болюсе через вкладку портала назначений. Убедитесь, что данному времени соответствует только одна запись о болюсе.</string> <string name="combo_error_multiple_boluses_with_identical_timestamp">Только что импортировано несколько болюсов с одинаковым количеством инсулина в течение одной минуты. В лог лечения может быть добавлена только одна запись. Пожалуйста проверьте помпу и вручную введите запись о болюсе через вкладку портала назначений. Убедитесь, что данному времени соответствует только одна запись о болюсе.</string>
<string name="combo_check_date">Новый болюс старше 24 часов или запись относится к будущему. Пожалуйста убедитесь что дата на помпе установлена правильно.</string> <string name="combo_check_date">Новый болюс старше 24 часов или запись относится к будущему. Убедитесь что дата на помпе установлена правильно.</string>
<string name="combo_suspious_bolus_time">Время/дата поданного болюса неверны. Вероятно, кол-во активного инсулина IOB также неверно. Проверьте время/дату помпы.</string> <string name="combo_suspious_bolus_time">Время/дата болюса неверны. Вероятно, кол-во активного инсулина IOB также неверно. Проверьте время/дату помпы.</string>
<string name="combo_bolus_count">Отсчет болюса</string> <string name="combo_bolus_count">Отсчет болюса</string>
<string name="combo_tbr_count">Отсчет временного базала TBR</string> <string name="combo_tbr_count">Отсчет временного базала TBR</string>
<string name="bolusstopped">Болюс остановлен</string> <string name="bolusstopped">Болюс остановлен</string>

View file

@ -153,7 +153,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.ClearAlarm, PumpHistoryEntryType.ClearAlarm,
PumpHistoryEntryType.ChangeAlarmNotifyMode, PumpHistoryEntryType.ChangeAlarmNotifyMode,
PumpHistoryEntryType.EnableDisableRemote, PumpHistoryEntryType.EnableDisableRemote,
PumpHistoryEntryType.BGReceived,
PumpHistoryEntryType.SensorAlert, PumpHistoryEntryType.SensorAlert,
PumpHistoryEntryType.ChangeTimeFormat, PumpHistoryEntryType.ChangeTimeFormat,
PumpHistoryEntryType.ChangeReservoirWarningTime, PumpHistoryEntryType.ChangeReservoirWarningTime,
@ -188,7 +187,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.ChangeWatchdogEnable, PumpHistoryEntryType.ChangeWatchdogEnable,
PumpHistoryEntryType.ChangeOtherDeviceID, PumpHistoryEntryType.ChangeOtherDeviceID,
PumpHistoryEntryType.ReadOtherDevicesIDs, PumpHistoryEntryType.ReadOtherDevicesIDs,
PumpHistoryEntryType.BGReceived512,
PumpHistoryEntryType.SensorStatus, PumpHistoryEntryType.SensorStatus,
PumpHistoryEntryType.ReadCaptureEventEnabled, PumpHistoryEntryType.ReadCaptureEventEnabled,
PumpHistoryEntryType.ChangeCaptureEventEnable, PumpHistoryEntryType.ChangeCaptureEventEnable,
@ -206,6 +204,12 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
PumpHistoryEntryType.UnabsorbedInsulin, PumpHistoryEntryType.UnabsorbedInsulin,
PumpHistoryEntryType.UnabsorbedInsulin512 -> RecordDecodeStatus.Ignored PumpHistoryEntryType.UnabsorbedInsulin512 -> RecordDecodeStatus.Ignored
PumpHistoryEntryType.BGReceived,
PumpHistoryEntryType.BGReceived512 -> {
decodeBgReceived(entry)
RecordDecodeStatus.OK
}
PumpHistoryEntryType.DailyTotals522, PumpHistoryEntryType.DailyTotals522,
PumpHistoryEntryType.DailyTotals523, PumpHistoryEntryType.DailyTotals523,
PumpHistoryEntryType.DailyTotals515, PumpHistoryEntryType.DailyTotals515,
@ -297,7 +301,9 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
} }
private fun decodeBatteryActivity(entry: PumpHistoryEntry) { 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 { private fun decodeBasalProfileStart(entry: PumpHistoryEntry): RecordDecodeStatus {
@ -407,8 +413,11 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
} }
private fun decodeBgReceived(entry: PumpHistoryEntry) { private fun decodeBgReceived(entry: PumpHistoryEntry) {
entry.addDecodedData("amount", (ByteUtil.asUINT8(entry.getRawDataByIndex(0)) shl 3) + (ByteUtil.asUINT8(entry.getRawDataByIndex(3)) shr 5)) val glucoseMgdl = (ByteUtil.asUINT8(entry.head[0]) shl 3) + (ByteUtil.asUINT8(entry.datetime[2]) shr 5)
entry.addDecodedData("meter", ByteUtil.substring(entry.rawData, 6, 3)) // index moved from 1 -> 0 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) { private fun decodeCalBGForPH(entry: PumpHistoryEntry) {

View file

@ -4,6 +4,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.notifications.Notification import app.aaps.core.interfaces.notifications.Notification
import app.aaps.core.interfaces.plugin.ActivePlugin 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.DetailedBolusInfo
import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.PumpSync
import app.aaps.core.interfaces.pump.defs.PumpType import app.aaps.core.interfaces.pump.defs.PumpType
@ -67,7 +68,8 @@ class MedtronicHistoryData @Inject constructor(
val medtronicPumpStatus: MedtronicPumpStatus, val medtronicPumpStatus: MedtronicPumpStatus,
private val pumpSync: PumpSync, private val pumpSync: PumpSync,
private val pumpSyncStorage: PumpSyncStorage, private val pumpSyncStorage: PumpSyncStorage,
private val uiInteraction: UiInteraction private val uiInteraction: UiInteraction,
private val profileUtil: ProfileUtil
) { ) {
val allHistory: MutableList<PumpHistoryEntry> = mutableListOf() val allHistory: MutableList<PumpHistoryEntry> = mutableListOf()
@ -322,6 +324,17 @@ class MedtronicHistoryData @Inject constructor(
* Process History Data: Boluses(Treatments), TDD, TBRs, Suspend-Resume (or other pump stops: battery, prime) * Process History Data: Boluses(Treatments), TDD, TBRs, Suspend-Resume (or other pump stops: battery, prime)
*/ */
fun processNewHistoryData() { fun processNewHistoryData() {
// Finger BG (for adding entry to careportal)
val bgRecords: MutableList<PumpHistoryEntry> = 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) // Prime (for resetting autosense)
val primeRecords: MutableList<PumpHistoryEntry> = getFilteredItems(PumpHistoryEntryType.Prime) val primeRecords: MutableList<PumpHistoryEntry> = getFilteredItems(PumpHistoryEntryType.Prime)
@ -347,6 +360,18 @@ class MedtronicHistoryData @Inject constructor(
} }
} }
// BatteryChange
val batteryChangeRecords: MutableList<PumpHistoryEntry> = 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 // TDD
val tdds: MutableList<PumpHistoryEntry> = getFilteredItems(setOf(PumpHistoryEntryType.EndResultTotals, getTDDType())) val tdds: MutableList<PumpHistoryEntry> = getFilteredItems(setOf(PumpHistoryEntryType.EndResultTotals, getTDDType()))
aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: TDD [count=%d, items=%s]", tdds.size, gson.toJson(tdds))) 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<PumpHistoryEntry>) {
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<PumpHistoryEntry>) { private fun processPrime(primeRecords: List<PumpHistoryEntry>) {
val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -30) val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -30)
var lastPrimeRecordTime = 0L var lastPrimeRecordTime = 0L
@ -456,6 +509,35 @@ class MedtronicHistoryData @Inject constructor(
} }
} }
private fun processBatteryChange(batteryChangeRecords: List<PumpHistoryEntry>) {
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) { private fun uploadCareportalEventIfFoundInHistory(historyRecord: PumpHistoryEntry, eventSP: String, eventType: DetailedBolusInfo.EventType) {
val lastPrimeFromAAPS = sp.getLong(eventSP, 0L) val lastPrimeFromAAPS = sp.getLong(eventSP, 0L)
if (historyRecord.atechDateTime != lastPrimeFromAAPS) { if (historyRecord.atechDateTime != lastPrimeFromAAPS) {

View file

@ -30,5 +30,6 @@ object MedtronicConst {
const val LastPumpHistoryEntry = StatsPrefix + "pump_history_entry" const val LastPumpHistoryEntry = StatsPrefix + "pump_history_entry"
const val LastPrime = StatsPrefix + "last_sent_prime" const val LastPrime = StatsPrefix + "last_sent_prime"
const val LastRewind = StatsPrefix + "last_sent_rewind" const val LastRewind = StatsPrefix + "last_sent_rewind"
const val LastBatteryChange = StatsPrefix + "last_sent_battery_change"
} }
} }

View file

@ -4,7 +4,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin
import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.PumpSync
import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.resources.ResourceHelper
import app.aaps.core.interfaces.sharedPreferences.SP 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.AndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil 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.Answers
import org.mockito.Mock import org.mockito.Mock
open class MedtronicTestBase : TestBase() { open class MedtronicTestBase : TestBaseWithProfile() {
var rileyLinkUtil = RileyLinkUtil() var rileyLinkUtil = RileyLinkUtil()
@Mock lateinit var pumpSync: PumpSync @Mock lateinit var pumpSync: PumpSync
@Mock lateinit var pumpSyncStorage: PumpSyncStorage @Mock lateinit var pumpSyncStorage: PumpSyncStorage
@Mock(answer = Answers.RETURNS_DEEP_STUBS) lateinit var activePlugin: ActivePlugin @Mock(answer = Answers.RETURNS_DEEP_STUBS) override lateinit var activePlugin: ActivePlugin
@Mock lateinit var sp: SP
@Mock lateinit var rh: ResourceHelper
lateinit var medtronicUtil: MedtronicUtil lateinit var medtronicUtil: MedtronicUtil
lateinit var decoder: MedtronicPumpHistoryDecoder lateinit var decoder: MedtronicPumpHistoryDecoder
@ -53,6 +51,24 @@ open class MedtronicTestBase : TestBase() {
} }
fun getPumpHistoryEntryFromData(vararg elements: Int): PumpHistoryEntry {
val data: MutableList<Byte> = 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<PumpHistoryEntry>): MutableList<PumpHistoryEntry> { private fun preProcessTBRs(tbrsInput: MutableList<PumpHistoryEntry>): MutableList<PumpHistoryEntry> {
val tbrs: MutableList<PumpHistoryEntry> = mutableListOf() val tbrs: MutableList<PumpHistoryEntry> = mutableListOf()
val map: MutableMap<String?, PumpHistoryEntry?> = HashMap() val map: MutableMap<String?, PumpHistoryEntry?> = HashMap()

View file

@ -40,7 +40,7 @@ import org.mockito.Mock
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
medtronicHistoryData = MedtronicHistoryData( medtronicHistoryData = MedtronicHistoryData(
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil
) )

View file

@ -1,8 +1,16 @@
package info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump 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.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 com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mock
import org.mockito.Mockito.`when`
/** /**
* Created by andy on 4/9/19. * Created by andy on 4/9/19.
@ -10,6 +18,16 @@ import org.junit.jupiter.api.Test
*/ */
class PumpHistoryEntryUTest : MedtronicTestBase() { 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 @Test
fun checkIsAfter() { fun checkIsAfter() {
val dateObject = 20191010000000L val dateObject = 20191010000000L
@ -18,4 +36,21 @@ class PumpHistoryEntryUTest : MedtronicTestBase() {
phe.atechDateTime = dateObject phe.atechDateTime = dateObject
assertThat(phe.isAfter(queryObject)).isTrue() 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)
}
} }

View file

@ -1,18 +1,24 @@
package info.nightscout.androidaps.plugins.pump.medtronic.data 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.interfaces.ui.UiInteraction
import app.aaps.core.utils.DateTimeUtil
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.internal.LinkedTreeMap import com.google.gson.internal.LinkedTreeMap
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase 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.MedtronicPumpHistoryDecoder
import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntry 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.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.driver.MedtronicPumpStatus
import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import java.lang.reflect.Type import java.lang.reflect.Type
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -24,6 +30,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction) medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction)
`when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel)
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
} }
@ -32,7 +39,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
val unitToTest = MedtronicHistoryData( val unitToTest = MedtronicHistoryData(
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil
) )
val gson = Gson() val gson = Gson()
@ -75,7 +82,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
medtronicUtil, decoder, medtronicUtil, decoder,
medtronicPumpStatus, medtronicPumpStatus,
pumpSync, pumpSync,
pumpSyncStorage, uiInteraction pumpSyncStorage, uiInteraction, profileUtil
) )
val gson = Gson() 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
)
}
} }

View file

@ -124,6 +124,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Probíhá nastavování (čeká se na aktivaci Podu)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Probíhá nastavování (čeká se na aktivaci Podu)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Probíhá nastavování (čeká se na aplikaci kanyly)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Probíhá nastavování (čeká se na aplikaci kanyly)</string>
<string name="omnipod_common_pod_status_running">Běží</string> <string name="omnipod_common_pod_status_running">Běží</string>
<string name="omnipod_common_pod_status_normal">Normální</string>
<string name="omnipod_common_pod_status_suspended">Pozastaveno</string> <string name="omnipod_common_pod_status_suspended">Pozastaveno</string>
<string name="omnipod_common_pod_status_pod_fault">Chyba Podu</string> <string name="omnipod_common_pod_status_pod_fault">Chyba Podu</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Byl překročen čas aktivace</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Byl překročen čas aktivace</string>

View file

@ -125,6 +125,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Configuración en curso (esperando para activar el Pod)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Configuración en curso (esperando para activar el Pod)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Configuración en curso (en espera de inserción de cánula)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Configuración en curso (en espera de inserción de cánula)</string>
<string name="omnipod_common_pod_status_running">Funcionando</string> <string name="omnipod_common_pod_status_running">Funcionando</string>
<string name="omnipod_common_pod_status_normal">Normal</string>
<string name="omnipod_common_pod_status_suspended">Suspendido</string> <string name="omnipod_common_pod_status_suspended">Suspendido</string>
<string name="omnipod_common_pod_status_pod_fault">Error del Pod</string> <string name="omnipod_common_pod_status_pod_fault">Error del Pod</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Tiempo de activación excedido</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Tiempo de activación excedido</string>

View file

@ -53,7 +53,7 @@
<string name="omnipod_common_overview_pod_unique_id">ID unique</string> <string name="omnipod_common_overview_pod_unique_id">ID unique</string>
<string name="omnipod_common_overview_lot_number">Numéro de lot</string> <string name="omnipod_common_overview_lot_number">Numéro de lot</string>
<string name="omnipod_common_overview_pod_sequence_number">Numéro de série</string> <string name="omnipod_common_overview_pod_sequence_number">Numéro de série</string>
<string name="omnipod_common_overview_pod_expiry_date">Pod Expiré</string> <string name="omnipod_common_overview_pod_expiry_date">Pod expire le</string>
<string name="omnipod_common_overview_last_connection">Dernière connexion</string> <string name="omnipod_common_overview_last_connection">Dernière connexion</string>
<string name="omnipod_common_overview_last_bolus">Dernier bolus</string> <string name="omnipod_common_overview_last_bolus">Dernier bolus</string>
<string name="omnipod_common_overview_temp_basal_rate">Débit de Basal Temp.</string> <string name="omnipod_common_overview_temp_basal_rate">Débit de Basal Temp.</string>

View file

@ -124,6 +124,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">התקנה בביצוע (ממתין להפעלת הפוד)</string> <string name="omnipod_common_pod_status_waiting_for_activation">התקנה בביצוע (ממתין להפעלת הפוד)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">התקנה בביצוע (ממתין להכנסת הפרפרית)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">התקנה בביצוע (ממתין להכנסת הפרפרית)</string>
<string name="omnipod_common_pod_status_running">פועל</string> <string name="omnipod_common_pod_status_running">פועל</string>
<string name="omnipod_common_pod_status_normal">נורמלי</string>
<string name="omnipod_common_pod_status_suspended">מושהה</string> <string name="omnipod_common_pod_status_suspended">מושהה</string>
<string name="omnipod_common_pod_status_pod_fault">תקלה בפוד</string> <string name="omnipod_common_pod_status_pod_fault">תקלה בפוד</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">חריגת זמן הפעלה</string> <string name="omnipod_common_pod_status_activation_time_exceeded">חריגת זמן הפעלה</string>

View file

@ -124,6 +124,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Oppsett pågår (venter på Pod-aktivering)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Oppsett pågår (venter på Pod-aktivering)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Oppsett pågår (venter på innsetting av kanyle)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Oppsett pågår (venter på innsetting av kanyle)</string>
<string name="omnipod_common_pod_status_running">Kjører</string> <string name="omnipod_common_pod_status_running">Kjører</string>
<string name="omnipod_common_pod_status_normal">Normal</string>
<string name="omnipod_common_pod_status_suspended">Pauset</string> <string name="omnipod_common_pod_status_suspended">Pauset</string>
<string name="omnipod_common_pod_status_pod_fault">Pod-feil</string> <string name="omnipod_common_pod_status_pod_fault">Pod-feil</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Aktiveringstiden er overskredet</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Aktiveringstiden er overskredet</string>

View file

@ -124,6 +124,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Setup wordt uitgevoerd (in afwachting van Pod activering)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Setup wordt uitgevoerd (in afwachting van Pod activering)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Setup wordt uitgevoerd (in afwachting van het inbrengen van de canule)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Setup wordt uitgevoerd (in afwachting van het inbrengen van de canule)</string>
<string name="omnipod_common_pod_status_running">Actief</string> <string name="omnipod_common_pod_status_running">Actief</string>
<string name="omnipod_common_pod_status_normal">Normaal</string>
<string name="omnipod_common_pod_status_suspended">Onderbroken</string> <string name="omnipod_common_pod_status_suspended">Onderbroken</string>
<string name="omnipod_common_pod_status_pod_fault">Pod fout</string> <string name="omnipod_common_pod_status_pod_fault">Pod fout</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Activatie tijd verlopen</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Activatie tijd verlopen</string>

View file

@ -124,6 +124,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Konfiguracja w toku (oczekiwanie na aktywację Poda)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Konfiguracja w toku (oczekiwanie na aktywację Poda)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Konfiguracja w toku (oczekiwanie na wprowadzenie kaniuli)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Konfiguracja w toku (oczekiwanie na wprowadzenie kaniuli)</string>
<string name="omnipod_common_pod_status_running">W działaniu</string> <string name="omnipod_common_pod_status_running">W działaniu</string>
<string name="omnipod_common_pod_status_normal">Normalny</string>
<string name="omnipod_common_pod_status_suspended">Wstrzymany</string> <string name="omnipod_common_pod_status_suspended">Wstrzymany</string>
<string name="omnipod_common_pod_status_pod_fault">Błąd Poda</string> <string name="omnipod_common_pod_status_pod_fault">Błąd Poda</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Przekroczono czas aktywacji</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Przekroczono czas aktywacji</string>

View file

@ -125,6 +125,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Setare în desfășurare (se așteaptă activarea Pod-ului)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Setare în desfășurare (se așteaptă activarea Pod-ului)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Inițializarea este în curs (se așteaptă inserarea canulei)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Inițializarea este în curs (se așteaptă inserarea canulei)</string>
<string name="omnipod_common_pod_status_running">Rulează</string> <string name="omnipod_common_pod_status_running">Rulează</string>
<string name="omnipod_common_pod_status_normal">Normal</string>
<string name="omnipod_common_pod_status_suspended">Suspendat</string> <string name="omnipod_common_pod_status_suspended">Suspendat</string>
<string name="omnipod_common_pod_status_pod_fault">Defecțiune Pod</string> <string name="omnipod_common_pod_status_pod_fault">Defecțiune Pod</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Timp de activare depăşit</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Timp de activare depăşit</string>

View file

@ -124,6 +124,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Выполняется настройка (ожидание активации Pod)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Выполняется настройка (ожидание активации Pod)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Выполняется настройка (ожидание ввода катетера)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Выполняется настройка (ожидание ввода катетера)</string>
<string name="omnipod_common_pod_status_running">Выполняется</string> <string name="omnipod_common_pod_status_running">Выполняется</string>
<string name="omnipod_common_pod_status_normal">Норма</string>
<string name="omnipod_common_pod_status_suspended">Приостановлено</string> <string name="omnipod_common_pod_status_suspended">Приостановлено</string>
<string name="omnipod_common_pod_status_pod_fault">Ошибка Pod</string> <string name="omnipod_common_pod_status_pod_fault">Ошибка Pod</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Превышено время активации</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Превышено время активации</string>

View file

@ -124,6 +124,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Prebieha inštalácia (čaká sa na aktiváciu Podu)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Prebieha inštalácia (čaká sa na aktiváciu Podu)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Inštalácia prebieha (čaká sa na vloženie kanyly)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Inštalácia prebieha (čaká sa na vloženie kanyly)</string>
<string name="omnipod_common_pod_status_running">V prevádzke</string> <string name="omnipod_common_pod_status_running">V prevádzke</string>
<string name="omnipod_common_pod_status_normal">Normálny</string>
<string name="omnipod_common_pod_status_suspended">Pozastavené</string> <string name="omnipod_common_pod_status_suspended">Pozastavené</string>
<string name="omnipod_common_pod_status_pod_fault">Chyba Podu</string> <string name="omnipod_common_pod_status_pod_fault">Chyba Podu</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Bol prekročený čas aktivácie</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Bol prekročený čas aktivácie</string>

View file

@ -124,6 +124,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Kurulum devam ediyor (Pod aktivasyonu bekleniyor)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Kurulum devam ediyor (Pod aktivasyonu bekleniyor)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Kurulum devam ediyor (kanül yerleştirme bekleniyor)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Kurulum devam ediyor (kanül yerleştirme bekleniyor)</string>
<string name="omnipod_common_pod_status_running">Çalışıyor</string> <string name="omnipod_common_pod_status_running">Çalışıyor</string>
<string name="omnipod_common_pod_status_normal">Normal</string>
<string name="omnipod_common_pod_status_suspended">Askıya Alındı</string> <string name="omnipod_common_pod_status_suspended">Askıya Alındı</string>
<string name="omnipod_common_pod_status_pod_fault">Pod Hatası</string> <string name="omnipod_common_pod_status_pod_fault">Pod Hatası</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Aktivasyon süresi aşıldı</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Aktivasyon süresi aşıldı</string>

View file

@ -153,6 +153,7 @@
<string name="omnipod_common_pod_status_waiting_for_activation">Setup in progress (waiting for Pod activation)</string> <string name="omnipod_common_pod_status_waiting_for_activation">Setup in progress (waiting for Pod activation)</string>
<string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Setup in progress (waiting for cannula insertion)</string> <string name="omnipod_common_pod_status_waiting_for_cannula_insertion">Setup in progress (waiting for cannula insertion)</string>
<string name="omnipod_common_pod_status_running">Running</string> <string name="omnipod_common_pod_status_running">Running</string>
<string name="omnipod_common_pod_status_normal">Normal</string>
<string name="omnipod_common_pod_status_suspended">Suspended</string> <string name="omnipod_common_pod_status_suspended">Suspended</string>
<string name="omnipod_common_pod_status_pod_fault">Pod Fault</string> <string name="omnipod_common_pod_status_pod_fault">Pod Fault</string>
<string name="omnipod_common_pod_status_activation_time_exceeded">Activation time exceeded</string> <string name="omnipod_common_pod_status_activation_time_exceeded">Activation time exceeded</string>

View file

@ -183,7 +183,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
if (!podStateManager.isPodRunning) { if (!podStateManager.isPodRunning) {
uiInteraction.addNotification( uiInteraction.addNotification(
Notification.OMNIPOD_POD_NOT_ATTACHED, 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 Notification.NORMAL
) )
} else { } else {
@ -991,9 +991,9 @@ class OmnipodDashPumpPlugin @Inject constructor(
val extended = JSONObject() val extended = JSONObject()
try { try {
val podStatus = when { val podStatus = when {
podStateManager.isPodRunning && podStateManager.isSuspended -> "suspended" podStateManager.isPodRunning && podStateManager.isSuspended -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_suspended).lowercase()
podStateManager.isPodRunning -> "normal" podStateManager.isPodRunning -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_normal).lowercase()
else -> "no active Pod" 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("status", podStatus)
status.put("timestamp", dateUtil.toISOString(podStateManager.lastUpdatedSystem)) status.put("timestamp", dateUtil.toISOString(podStateManager.lastUpdatedSystem))

View file

@ -36,7 +36,7 @@ import org.mockito.invocation.InvocationOnMock
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
open class TestBaseWithProfile : TestBase() { open class TestBaseWithProfile : TestBase() {
@Mock lateinit var activePlugin: ActivePlugin @Mock open lateinit var activePlugin: ActivePlugin
@Mock lateinit var rh: ResourceHelper @Mock lateinit var rh: ResourceHelper
@Mock lateinit var iobCobCalculator: IobCobCalculator @Mock lateinit var iobCobCalculator: IobCobCalculator
@Mock lateinit var fabricPrivacy: FabricPrivacy @Mock lateinit var fabricPrivacy: FabricPrivacy

View file

@ -284,31 +284,6 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/done_background"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_vertical"
android:background="@android:color/transparent"
android:orientation="horizontal"
android:paddingStart="5dp"
android:paddingEnd="5dp">
<CheckBox
android:id="@+id/calculation_checkbox"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:checked="false"
android:contentDescription="@string/show_calculation"
android:drawableEnd="@drawable/ic_visibility" />
<include
android:id="@+id/okcancel"
layout="@layout/okcancel" />
</LinearLayout>
<View <View
android:id="@+id/delimiter" android:id="@+id/delimiter"
android:layout_width="fill_parent" android:layout_width="fill_parent"
@ -683,6 +658,31 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/done_background"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_vertical"
android:background="@android:color/transparent"
android:orientation="horizontal"
android:paddingStart="5dp"
android:paddingEnd="5dp">
<CheckBox
android:id="@+id/calculation_checkbox"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:checked="false"
android:contentDescription="@string/show_calculation"
android:drawableEnd="@drawable/ic_visibility" />
<include
android:id="@+id/okcancel"
layout="@layout/okcancel" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View file

@ -42,7 +42,7 @@
<string name="record_pump_site_change">Внести запись о смене места катетера помпы</string> <string name="record_pump_site_change">Внести запись о смене места катетера помпы</string>
<string name="record_insulin_cartridge_change">Внести запись о замене картриджа инсулина</string> <string name="record_insulin_cartridge_change">Внести запись о замене картриджа инсулина</string>
<!-- InsulinDialog --> <!-- InsulinDialog -->
<string name="do_not_bolus_record_only">Не подавать болюс, только внести запись</string> <string name="do_not_bolus_record_only">Не вводить болюс, только внести запись</string>
<!-- ProfileSwitchDialog --> <!-- ProfileSwitchDialog -->
<string name="reuse_profile_pct_hours">Повторно использовать %1$d%%%2$dч</string> <string name="reuse_profile_pct_hours">Повторно использовать %1$d%%%2$dч</string>
<string name="timeshift_label">Сдвиг по времени</string> <string name="timeshift_label">Сдвиг по времени</string>

Some files were not shown because too many files have changed in this diff Show more