commit
bb1a276078
102 changed files with 2700 additions and 217 deletions
|
@ -5,7 +5,7 @@ version: 2.1
|
|||
# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
|
||||
orbs:
|
||||
android: circleci/android@2.3.0
|
||||
codecov: codecov/codecov@3.2.4
|
||||
codecov: codecov/codecov@3.3.0
|
||||
|
||||
jobs:
|
||||
# Below is the definition of your job to build and test your app, you can rename and customize it as you want.
|
||||
|
@ -14,7 +14,7 @@ jobs:
|
|||
executor:
|
||||
name: android/android-machine
|
||||
resource-class: large
|
||||
tag: 2023.07.1
|
||||
tag: 2023.11.1
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
@ -34,11 +34,19 @@ jobs:
|
|||
- android/run-tests:
|
||||
test-command: ./gradlew --stacktrace jacocoAllDebugReport
|
||||
|
||||
# And finally run the release build
|
||||
# - run:
|
||||
# name: Assemble release build
|
||||
# command: |
|
||||
# ./gradlew assembleRelease
|
||||
- run:
|
||||
name: Save test results
|
||||
command: |
|
||||
mkdir -p ~/test-results/junit/
|
||||
find . -type f -regex ".*/build/outputs/androidTest-results/.*xml" -exec cp {} ~/test-results/junit/ \;
|
||||
when: always
|
||||
|
||||
- store_test_results:
|
||||
path: ~/test-results
|
||||
|
||||
- store_artifacts:
|
||||
path: ~/test-results/junit
|
||||
|
||||
- codecov/upload:
|
||||
file: './build/reports/jacoco/jacocoAllDebugReport/jacocoAllDebugReport.xml'
|
||||
|
||||
|
|
|
@ -153,6 +153,7 @@ android {
|
|||
//Deleting it causes a binding error
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -276,7 +276,7 @@ class MainActivity : DaggerAppCompatActivityWithResult() {
|
|||
})
|
||||
// Setup views on 2nd and next activity start
|
||||
// On 1st start app is still initializing, start() is delayed and run from EventAppInitialized
|
||||
if (config.appInitialized) start()
|
||||
if (config.appInitialized) setupViews()
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
|
|
|
@ -47,7 +47,7 @@ import app.aaps.plugins.source.PoctechPlugin
|
|||
import app.aaps.plugins.source.RandomBgPlugin
|
||||
import app.aaps.plugins.source.TomatoPlugin
|
||||
import app.aaps.plugins.source.XdripSourcePlugin
|
||||
import app.aaps.plugins.sync.dataBroadcaster.DataBroadcastPlugin
|
||||
import app.aaps.plugins.sync.tizen.TizenPlugin
|
||||
import app.aaps.plugins.sync.nsclient.NSClientPlugin
|
||||
import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin
|
||||
import app.aaps.plugins.sync.openhumans.OpenHumansUploaderPlugin
|
||||
|
@ -350,7 +350,7 @@ abstract class PluginsListModule {
|
|||
@AllConfigs
|
||||
@IntoMap
|
||||
@IntKey(368)
|
||||
abstract fun bindDataBroadcastPlugin(plugin: DataBroadcastPlugin): PluginBase
|
||||
abstract fun bindDataBroadcastPlugin(plugin: TizenPlugin): PluginBase
|
||||
|
||||
@Binds
|
||||
@AllConfigs
|
||||
|
|
|
@ -8,7 +8,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
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.firebase:firebase-crashlytics-gradle:2.9.9")
|
||||
|
||||
|
@ -22,7 +22,7 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "12.0.3"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
object KtsBuildVersions {
|
||||
|
||||
const val gradle = "8.1.3"
|
||||
const val kotlin = "1.9.0"
|
||||
const val gradle = "8.2.0"
|
||||
const val kotlin = "1.9.10"
|
||||
}
|
||||
|
||||
plugins {
|
||||
|
|
|
@ -152,8 +152,11 @@ object Libs {
|
|||
}
|
||||
|
||||
object Mockito {
|
||||
private const val mockitoVersion = "5.6.0"
|
||||
|
||||
const val jupiter = "org.mockito:mockito-junit-jupiter:5.6.0"
|
||||
const val android = "org.mockito:mockito-android:$mockitoVersion"
|
||||
const val core = "org.mockito:mockito-core:$mockitoVersion"
|
||||
const val jupiter = "org.mockito:mockito-junit-jupiter:$mockitoVersion"
|
||||
const val kotlin = "org.mockito.kotlin:mockito-kotlin:5.1.0"
|
||||
}
|
||||
|
||||
|
@ -197,19 +200,5 @@ object Libs {
|
|||
const val commonCodecs = "commons-codec:commons-codec:1.16.0"
|
||||
const val kulid = "com.github.guepardoapps:kulid:2.0.0.0"
|
||||
const val xstream = "com.thoughtworks.xstream:xstream:1.4.20"
|
||||
|
||||
const val ormLite = "4.46"
|
||||
|
||||
const val junit = "4.13.2"
|
||||
const val mockito = "5.6.0"
|
||||
const val dexmaker = "1.2"
|
||||
const val byteBuddy = "1.12.8"
|
||||
|
||||
const val androidx_junit = "1.1.5"
|
||||
const val androidx_rules = "1.5.0"
|
||||
|
||||
const val kotlinx_datetime = "0.4.1"
|
||||
const val kotlinx_serialization = "1.6.0"
|
||||
|
||||
const val caverock_androidsvg = "1.4"
|
||||
const val connectiqSdk = "com.garmin.connectiq:ciq-companion-app-sdk:2.0.3@aar"
|
||||
}
|
|
@ -2,7 +2,7 @@ import org.gradle.api.JavaVersion
|
|||
|
||||
object Versions {
|
||||
|
||||
const val appVersion = "3.2.0.2"
|
||||
const val appVersion = "3.2.0.3"
|
||||
const val versionCode = 1500
|
||||
|
||||
const val ndkVersion = "21.1.6352462"
|
||||
|
|
|
@ -22,6 +22,9 @@ dependencies {
|
|||
androidTestImplementation(Libs.AndroidX.Test.rules)
|
||||
androidTestImplementation(Libs.Google.truth)
|
||||
androidTestImplementation(Libs.AndroidX.Test.uiAutomator)
|
||||
androidTestImplementation(Libs.Mockito.core)
|
||||
androidTestImplementation(Libs.Mockito.android)
|
||||
androidTestImplementation(Libs.Mockito.kotlin)
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package app.aaps.core.interfaces.pump
|
||||
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.profile.Profile
|
||||
import app.aaps.core.interfaces.pump.defs.PumpType
|
||||
import app.aaps.core.interfaces.utils.DateUtil
|
||||
|
@ -257,6 +258,26 @@ interface PumpSync {
|
|||
**/
|
||||
fun insertTherapyEventIfNewWithTimestamp(timestamp: Long, type: DetailedBolusInfo.EventType, note: String? = null, pumpId: Long? = null, pumpType: PumpType, pumpSerial: String): Boolean
|
||||
|
||||
/**
|
||||
* Synchronization of FINGER_STICK_BG_VALUE events
|
||||
*
|
||||
* Assuming there will be no clash on timestamp from different pumps
|
||||
* only timestamp and type is compared
|
||||
*
|
||||
* If db record doesn't exist, new record is created.
|
||||
* If exists, data is ignored
|
||||
*
|
||||
* @param timestamp timestamp of event from pump history
|
||||
* @param glucose glucose value
|
||||
* @param glucoseUnit glucose unit
|
||||
* @param note note
|
||||
* @param pumpId pump id from history if available
|
||||
* @param pumpType pump type like PumpType.ACCU_CHEK_COMBO
|
||||
* @param pumpSerial pump serial number
|
||||
* @return true if new record is created
|
||||
**/
|
||||
fun insertFingerBgIfNewWithTimestamp(timestamp: Long, glucose: Double, glucoseUnit: GlucoseUnit, note: String? = null, pumpId: Long? = null, pumpType: PumpType, pumpSerial: String): Boolean
|
||||
|
||||
/**
|
||||
* Create an announcement
|
||||
*
|
||||
|
|
|
@ -25,6 +25,7 @@ interface Intents {
|
|||
const val EXTRA_BG_SLOPE = "com.eveningoutpost.dexdrip.Extras.BgSlope"
|
||||
const val EXTRA_BG_SLOPE_NAME = "com.eveningoutpost.dexdrip.Extras.BgSlopeName"
|
||||
const val EXTRA_SENSOR_BATTERY = "com.eveningoutpost.dexdrip.Extras.SensorBattery"
|
||||
const val EXTRA_SENSOR_STARTED_AT = "com.eveningoutpost.dexdrip.Extras.SensorStartedAt"
|
||||
const val EXTRA_TIMESTAMP = "com.eveningoutpost.dexdrip.Extras.Time"
|
||||
const val EXTRA_RAW = "com.eveningoutpost.dexdrip.Extras.Raw"
|
||||
const val XDRIP_DATA_SOURCE_DESCRIPTION = "com.eveningoutpost.dexdrip.Extras.SourceDesc"
|
||||
|
|
|
@ -1,22 +1,56 @@
|
|||
package app.aaps.core.utils
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
object MidnightUtils {
|
||||
/*
|
||||
/**
|
||||
* Midnight time conversion
|
||||
*/
|
||||
object MidnightUtils {
|
||||
|
||||
/**
|
||||
* Actual passed seconds from midnight ignoring DST change
|
||||
* (thus always having 24 hours in a day, not 23 or 25 in days where DST changes)
|
||||
*
|
||||
* @return seconds
|
||||
*/
|
||||
fun secondsFromMidnight(): Int {
|
||||
val passed = DateTime().millisOfDay.toLong()
|
||||
return (passed / 1000).toInt()
|
||||
val nowZoned = ZonedDateTime.now()
|
||||
val localTime = nowZoned.toLocalTime()
|
||||
val midnight = nowZoned.toLocalDate().atStartOfDay(nowZoned.zone).toLocalTime()
|
||||
val duration = Duration.between(midnight, localTime)
|
||||
return duration.seconds.toInt()
|
||||
}
|
||||
|
||||
fun secondsFromMidnight(date: Long): Int {
|
||||
val passed = DateTime(date).millisOfDay.toLong()
|
||||
return (passed / 1000).toInt()
|
||||
/**
|
||||
* Passed seconds from midnight for specified time ignoring DST change
|
||||
* (thus always having 24 hours in a day, not 23 or 25 in days where DST changes)
|
||||
*
|
||||
* @param timestamp time
|
||||
* @return seconds
|
||||
*/
|
||||
fun secondsFromMidnight(timestamp: Long): Int {
|
||||
val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
|
||||
val localTime = timeZoned.toLocalTime()
|
||||
val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime()
|
||||
val duration: Duration = Duration.between(midnight, localTime)
|
||||
return duration.seconds.toInt()
|
||||
}
|
||||
|
||||
fun milliSecFromMidnight(date: Long): Long {
|
||||
return DateTime(date).millisOfDay.toLong()
|
||||
/**
|
||||
* Passed milliseconds from midnight for specified time ignoring DST change
|
||||
* (thus always having 24 hours in a day, not 23 or 25 in days where DST changes)
|
||||
*
|
||||
* @param timestamp time
|
||||
* @return milliseconds
|
||||
*/
|
||||
fun milliSecFromMidnight(timestamp: Long): Long {
|
||||
val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
|
||||
val localTime = timeZoned.toLocalTime()
|
||||
val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime()
|
||||
val duration = Duration.between(midnight, localTime)
|
||||
return duration.toMillis()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
org.gradle.parallel=true
|
||||
org.gradle.warning.mode=all
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+UseParallelGC
|
||||
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
|
||||
|
||||
android.enableJetifier=false
|
||||
android.useAndroidX=true
|
||||
|
@ -32,5 +32,4 @@ android.nonTransitiveRClass=true
|
|||
# null: KtCallExpression
|
||||
# https://youtrack.jetbrains.com/issue/KT-58027
|
||||
kapt.use.jvm.ir=false
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonFinalResIds=true
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package app.aaps.implementation.pump
|
||||
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.logging.AAPSLogger
|
||||
import app.aaps.core.interfaces.logging.LTag
|
||||
import app.aaps.core.interfaces.logging.UserEntryLogger
|
||||
|
@ -16,6 +17,7 @@ import app.aaps.core.interfaces.sharedPreferences.SP
|
|||
import app.aaps.core.interfaces.utils.DateUtil
|
||||
import app.aaps.core.interfaces.utils.T
|
||||
import app.aaps.core.main.events.EventNewNotification
|
||||
import app.aaps.core.main.extensions.fromConstant
|
||||
import app.aaps.core.main.pump.fromDbPumpType
|
||||
import app.aaps.core.main.pump.toDbPumpType
|
||||
import app.aaps.core.main.pump.toDbSource
|
||||
|
@ -291,6 +293,42 @@ class PumpSyncImplementation @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun insertFingerBgIfNewWithTimestamp(timestamp: Long, glucose: Double, glucoseUnit: GlucoseUnit, note: String?, pumpId: Long?, pumpType: PumpType, pumpSerial: String): Boolean {
|
||||
if (!confirmActivePump(timestamp, pumpType, pumpSerial)) return false
|
||||
var type = TherapyEvent.Type.FINGER_STICK_BG_VALUE
|
||||
val therapyEvent = TherapyEvent(
|
||||
timestamp = timestamp,
|
||||
type = type,
|
||||
duration = 0,
|
||||
note = note,
|
||||
enteredBy = "AndroidAPS",
|
||||
glucose = glucose,
|
||||
glucoseType = TherapyEvent.MeterType.FINGER,
|
||||
glucoseUnit = TherapyEvent.GlucoseUnit.fromConstant(glucoseUnit),
|
||||
interfaceIDs_backing = InterfaceIDs(
|
||||
pumpId = pumpId,
|
||||
pumpType = pumpType.toDbPumpType(),
|
||||
pumpSerial = pumpSerial
|
||||
)
|
||||
)
|
||||
uel.log(
|
||||
action = UserEntry.Action.CAREPORTAL,
|
||||
source = pumpType.source.toDbSource(),
|
||||
note = note,
|
||||
timestamp = timestamp,
|
||||
ValueWithUnit.Timestamp(timestamp), ValueWithUnit.TherapyEventType(type)
|
||||
)
|
||||
repository.runTransactionForResult(InsertIfNewByTimestampTherapyEventTransaction(therapyEvent))
|
||||
.doOnError {
|
||||
aapsLogger.error(LTag.DATABASE, "Error while saving TherapyEvent", it)
|
||||
}
|
||||
.blockingGet()
|
||||
.also { result ->
|
||||
result.inserted.forEach { aapsLogger.debug(LTag.DATABASE, "Inserted TherapyEvent $it") }
|
||||
return result.inserted.size > 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun insertAnnouncement(error: String, pumpId: Long?, pumpType: PumpType, pumpSerial: String) {
|
||||
if (!confirmActivePump(dateUtil.now(), pumpType, pumpSerial)) return
|
||||
disposable += repository.runTransaction(InsertTherapyEventAnnouncementTransaction(error, pumpId, pumpType.toDbPumpType(), pumpSerial))
|
||||
|
|
|
@ -119,13 +119,13 @@ class CommandQueueImplementation @Inject constructor(
|
|||
return@subscribe
|
||||
}
|
||||
aapsLogger.debug(LTag.PROFILE, "onEventProfileSwitchChanged")
|
||||
val effective = repository.getEffectiveProfileSwitchActiveAt(dateUtil.now()).blockingGet()
|
||||
profileFunction.getRequestedProfile()?.let {
|
||||
setProfile(ProfileSealed.PS(it), it.interfaceIDs.nightscoutId != null, object : Callback() {
|
||||
override fun run() {
|
||||
if (!result.success) {
|
||||
uiInteraction.runAlarm(result.comment, rh.gs(app.aaps.core.ui.R.string.failed_update_basal_profile), app.aaps.core.ui.R.raw.boluserror)
|
||||
} else if (result.enacted || effective is ValueWrapper.Existing && effective.value.originalEnd < dateUtil.now() && effective.value.originalDuration != 0L) {
|
||||
} else /* if (result.enacted || effective is ValueWrapper.Existing && effective.value.originalEnd < dateUtil.now() && effective.value.originalDuration != 0L) */ {
|
||||
// Pump may return enacted == false if basal profile is the same, but IC/ISF can be different
|
||||
val nonCustomized = ProfileSealed.PS(it).convertToNonCustomizedProfile(dateUtil)
|
||||
EffectiveProfileSwitch(
|
||||
timestamp = dateUtil.now(),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="alert_r1_title">Дать болюс</string>
|
||||
<string name="alert_r2_title">Недоставленный болюс</string>
|
||||
<string name="alert_r1_title">Ввести болюс</string>
|
||||
<string name="alert_r2_title">Несостоявшийся болюс</string>
|
||||
<string name="alert_r3_title">Будильник</string>
|
||||
<string name="alert_r4_title">Замените инфузионный набор</string>
|
||||
<string name="alert_r7_title">TBR завершен</string>
|
||||
|
|
|
@ -79,6 +79,7 @@ import dagger.android.HasAndroidInjector
|
|||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.abs
|
||||
|
@ -135,6 +136,8 @@ class LoopPlugin @Inject constructor(
|
|||
disposable += rxBus
|
||||
.toObservable(EventTempTargetChange::class.java)
|
||||
.observeOn(aapsSchedulers.io)
|
||||
// Skip db change of ending previous TT
|
||||
.debounce(10L, TimeUnit.SECONDS)
|
||||
.subscribe({ invoke("EventTempTargetChange", true) }, fabricPrivacy::logException)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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_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>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_summary" formatted="false">כיוונון פקטור הרגישות הדינאמית. הגדירו יותר מ-100% לקבלת תיקונים אגרסיביים יותר ופחות מ-100% לקבלת תיקונים עדינים יותר.</string>
|
||||
<string name="high_temptarget_raises_sensitivity_title">ערך מטרה זמני גבוה מעלה את הרגישות</string>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
<string name="low_temptarget_lowers_sensitivity_summary"><![CDATA[Понизить чувствительность для temptargets > = 100]]></string>
|
||||
<string name="openapssmb">OpenAPS СМБ</string>
|
||||
<string name="openaps_smb_dynamic_isf">Динамический ISF</string>
|
||||
<string name="smb_interval_summary">Как часто СМБ будут подаваться в минутах</string>
|
||||
<string name="resistance_lowers_target_title">Сопротивляемость понижает цель</string>
|
||||
<string name="resistance_lowers_target_summary">При обнаружении сопротивляемости целевой уровень гликемии понижается</string>
|
||||
<string name="smb_interval_summary">Как часто SMB будут подаваться в минутах</string>
|
||||
<string name="resistance_lowers_target_title">Резистентность понижает цель</string>
|
||||
<string name="resistance_lowers_target_summary">При обнаружении резистентности целевой уровень гликемии понижается</string>
|
||||
<string name="sensitivity_raises_target_title">Чувствительность поднимает цель</string>
|
||||
<string name="sensitivity_raises_target_summary">При обнаружении чувствительности целевой уровень глюкозы повышается</string>
|
||||
<string name="openapsma_disabled">модуль не активен</string>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<string name="diawarning">Напоминание: новые профили инсулина требуют как минимум 5 часов длительности действия DIA. 5-6 часовая DIA эквивалентна трехчасовой на старых профилях.</string>
|
||||
<string name="profile">Профиль</string>
|
||||
<string name="doprofileswitch">Переключить профиль</string>
|
||||
<string name="exitwizard">Пропустить Мастер установки</string>
|
||||
<string name="exitwizard">Пропустить Мастер настройки</string>
|
||||
<string name="setupwizard_loop_description">Нажмите на кнопку ниже, чтобы AAPS мог предложить/внести изменения в базал</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>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package app.aaps.plugins.constraints.versionChecker
|
||||
|
||||
import org.joda.time.LocalDate
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
|
||||
class AllowedVersions {
|
||||
|
||||
|
@ -38,7 +40,8 @@ class AllowedVersions {
|
|||
fun endDateToMilliseconds(endDate: String): Long? {
|
||||
try {
|
||||
val dateTime = LocalDate.parse(endDate)
|
||||
return dateTime.toDate().time
|
||||
val instant = dateTime.atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant()
|
||||
return instant.toEpochMilli()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
return null
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<string name="version_expire">Versjon %1$s utløper den %2$s</string>
|
||||
<!-- Signature verifier -->
|
||||
<!-- 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="a11y_bg_quality_recalculated">beregnet på nytt</string>
|
||||
<string name="a11y_bg_quality_doubles">doble registreringer</string>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<resources>
|
||||
<string name="objectives_button_start">Start</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="objectivenotfinished">Zadanie %1$d nieukończone</string>
|
||||
<string name="objectives_0_objective">Konfiguracja wizualizacji i monitorowania, analiza dawek bazowych i współczynników</string>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<string name="objectives_openloop_gate">Используйте режим Открытого цикла на протяжении нескольких дней и вручную выставляйте временные цели. Настройте ВЦ по умолчанию (ВЦ для Нагрузки, Гипо, Ожидания приема пищи) и используйте их.</string>
|
||||
<string name="objectives_openloop_learned">Открытый цикл может быть использован для получения рекомендаций по терапии, если у вас нет совместимой помпы или если вы не готовы закрыть цикл.</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_maxiobzero_objective">Начинаем замыкать цикл с прекращением подачи инсулина при низких значениях ГК (режим Low Glucose Suspend)</string>
|
||||
<string name="objectives_maxiobzero_gate">Работа в замкнутом цикле с maxIOB = 0 на протяжении нескольких дней. Старайтесь избегать низкой ГК, чтобы не вызывать события приостановки подачи инсулина на низких сахарах.</string>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package app.aaps.plugins.constraints.versionChecker
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import app.aaps.plugins.constraints.versionChecker.AllowedVersions
|
||||
import org.joda.time.LocalDate
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
class AllowedVersionsTest {
|
||||
|
||||
|
@ -71,10 +72,10 @@ class AllowedVersionsTest {
|
|||
@Test
|
||||
fun endDateToMilliseconds() {
|
||||
val definition = generateSupportedVersions()
|
||||
val endDate = AllowedVersions().endDateToMilliseconds(AllowedVersions().findByVersion(definition, "2.9.0-beta1")?.getString("endDate") ?: "1000/01/01")
|
||||
val dateTime = LocalDate(endDate)
|
||||
val endDate = AllowedVersions().endDateToMilliseconds(AllowedVersions().findByVersion(definition, "2.9.0-beta1")?.getString("endDate") ?: "1000/01/01") ?: 0L
|
||||
val dateTime = LocalDate.ofInstant(Instant.ofEpochMilli(endDate), ZoneId.systemDefault())
|
||||
assertThat(dateTime.year).isEqualTo(2021)
|
||||
assertThat(dateTime.monthOfYear).isEqualTo(11)
|
||||
assertThat(dateTime.monthValue).isEqualTo(11)
|
||||
assertThat(dateTime.dayOfMonth).isEqualTo(7)
|
||||
|
||||
assertThat(AllowedVersions().endDateToMilliseconds("abdef")).isNull()
|
||||
|
|
|
@ -124,7 +124,7 @@ class PersistentNotificationPlugin @Inject constructor(
|
|||
val lastBG = iobCobCalculator.ads.lastBg()
|
||||
val glucoseStatus = glucoseStatusProvider.glucoseStatusData
|
||||
if (lastBG != null) {
|
||||
line1aa = profileUtil.fromMgdlToStringInUnits(lastBG.value)
|
||||
line1aa = profileUtil.fromMgdlToStringInUnits(lastBG.recalculated)
|
||||
line1 = line1aa
|
||||
if (glucoseStatus != null) {
|
||||
line1 += (" Δ" + profileUtil.fromMgdlToSignedStringInUnits(glucoseStatus.delta)
|
||||
|
|
|
@ -356,11 +356,11 @@ class SmsCommunicatorPlugin @Inject constructor(
|
|||
var reply = ""
|
||||
val units = profileUtil.units
|
||||
if (actualBG != null) {
|
||||
reply = rh.gs(R.string.sms_actual_bg) + " " + profileUtil.fromMgdlToStringInUnits(actualBG.value) + ", "
|
||||
reply = rh.gs(R.string.sms_actual_bg) + " " + profileUtil.fromMgdlToStringInUnits(actualBG.recalculated) + ", "
|
||||
} else if (lastBG != null) {
|
||||
val agoMilliseconds = dateUtil.now() - lastBG.timestamp
|
||||
val agoMin = (agoMilliseconds / 60.0 / 1000.0).toInt()
|
||||
reply = rh.gs(R.string.sms_last_bg) + " " + profileUtil.valueInCurrentUnitsDetect(lastBG.value) + " " + rh.gs(R.string.sms_min_ago, agoMin) + ", "
|
||||
reply = rh.gs(R.string.sms_last_bg) + " " + profileUtil.valueInCurrentUnitsDetect(lastBG.recalculated) + " " + rh.gs(R.string.sms_min_ago, agoMin) + ", "
|
||||
}
|
||||
val glucoseStatus = glucoseStatusProvider.glucoseStatusData
|
||||
if (glucoseStatus != null) reply += rh.gs(R.string.sms_delta) + " " + profileUtil.fromMgdlToUnits(glucoseStatus.delta) + " " + units + ", "
|
||||
|
|
|
@ -300,7 +300,7 @@ class ProfileFragment : DaggerFragment() {
|
|||
|
||||
binding.profileRemove.setOnClickListener {
|
||||
activity?.let { activity ->
|
||||
OKDialog.showConfirmation(activity, rh.gs(R.string.delete_current_profile), {
|
||||
OKDialog.showConfirmation(activity, rh.gs(R.string.delete_current_profile, profilePlugin.currentProfile()?.name), {
|
||||
uel.log(
|
||||
UserEntry.Action.PROFILE_REMOVED, UserEntry.Sources.LocalProfile, ValueWithUnit.SimpleString(
|
||||
profilePlugin.currentProfile()?.name
|
||||
|
|
|
@ -211,7 +211,7 @@
|
|||
<string name="change_your_input">Endre dine inndata!</string>
|
||||
<string name="openaps">OpenAPS</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="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>
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<resources>
|
||||
<string name="smoothing_shortname">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="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="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>
|
||||
|
|
|
@ -22,6 +22,8 @@ dependencies {
|
|||
implementation(project(":core:ui"))
|
||||
implementation(project(":core:utils"))
|
||||
|
||||
testImplementation(Libs.AndroidX.Work.testing)
|
||||
|
||||
testImplementation(project(":shared:tests"))
|
||||
|
||||
kapt(Libs.Dagger.compiler)
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
package app.aaps.plugins.source
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import app.aaps.core.interfaces.logging.AAPSLogger
|
||||
import app.aaps.core.interfaces.logging.LTag
|
||||
import app.aaps.core.interfaces.logging.UserEntryLogger
|
||||
import app.aaps.core.interfaces.plugin.PluginBase
|
||||
import app.aaps.core.interfaces.plugin.PluginDescription
|
||||
import app.aaps.core.interfaces.plugin.PluginType
|
||||
import app.aaps.core.interfaces.receivers.Intents
|
||||
import app.aaps.core.interfaces.resources.ResourceHelper
|
||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
import app.aaps.core.interfaces.source.BgSource
|
||||
import app.aaps.core.interfaces.source.XDripSource
|
||||
import app.aaps.core.interfaces.utils.DateUtil
|
||||
import app.aaps.core.interfaces.utils.T
|
||||
import app.aaps.core.main.utils.worker.LoggingWorker
|
||||
import app.aaps.core.utils.receivers.DataWorkerStorage
|
||||
import app.aaps.database.entities.GlucoseValue
|
||||
import app.aaps.database.entities.UserEntry.Action
|
||||
import app.aaps.database.entities.UserEntry.Sources
|
||||
import app.aaps.database.entities.ValueWithUnit
|
||||
import app.aaps.database.impl.AppRepository
|
||||
import app.aaps.database.impl.transactions.CgmSourceTransaction
|
||||
import app.aaps.database.transactions.TransactionGlucoseValue
|
||||
|
@ -22,6 +30,7 @@ import dagger.android.HasAndroidInjector
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.round
|
||||
|
||||
@Singleton
|
||||
|
@ -34,7 +43,7 @@ class XdripSourcePlugin @Inject constructor(
|
|||
.mainType(PluginType.BGSOURCE)
|
||||
.fragmentClass(BGSourceFragment::class.java.name)
|
||||
.pluginIcon((app.aaps.core.main.R.drawable.ic_blooddrop_48))
|
||||
.preferencesId(R.xml.pref_bgsource)
|
||||
.preferencesId(R.xml.pref_dexcom)
|
||||
.pluginName(R.string.source_xdrip)
|
||||
.description(R.string.description_source_xdrip),
|
||||
aapsLogger, rh, injector
|
||||
|
@ -63,8 +72,25 @@ class XdripSourcePlugin @Inject constructor(
|
|||
) : LoggingWorker(context, params, Dispatchers.IO) {
|
||||
|
||||
@Inject lateinit var xdripSourcePlugin: XdripSourcePlugin
|
||||
@Inject lateinit var sp: SP
|
||||
@Inject lateinit var dateUtil: DateUtil
|
||||
@Inject lateinit var repository: AppRepository
|
||||
@Inject lateinit var dataWorkerStorage: DataWorkerStorage
|
||||
@Inject lateinit var uel: UserEntryLogger
|
||||
|
||||
fun getSensorStartTime(bundle: Bundle): Long? {
|
||||
val now = dateUtil.now()
|
||||
var sensorStartTime: Long? = if (sp.getBoolean(R.string.key_dexcom_log_ns_sensor_change, false)) {
|
||||
bundle.getLong(Intents.EXTRA_SENSOR_STARTED_AT, 0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
// check start time validity
|
||||
sensorStartTime?.let {
|
||||
if (abs(it - now) > T.months(1).msecs() || it > now) sensorStartTime = null
|
||||
}
|
||||
return sensorStartTime
|
||||
}
|
||||
|
||||
override suspend fun doWorkAndLog(): Result {
|
||||
var ret = Result.success()
|
||||
|
@ -86,17 +112,27 @@ class XdripSourcePlugin @Inject constructor(
|
|||
?: ""
|
||||
)
|
||||
)
|
||||
repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), null))
|
||||
val sensorStartTime = getSensorStartTime(bundle)
|
||||
repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), sensorStartTime))
|
||||
.doOnError {
|
||||
aapsLogger.error(LTag.DATABASE, "Error while saving values from Xdrip", it)
|
||||
ret = Result.failure(workDataOf("Error" to it.toString()))
|
||||
}
|
||||
.blockingGet()
|
||||
.also { savedValues ->
|
||||
savedValues.all().forEach {
|
||||
.also { result ->
|
||||
result.all().forEach {
|
||||
xdripSourcePlugin.detectSource(it)
|
||||
aapsLogger.debug(LTag.DATABASE, "Inserted bg $it")
|
||||
}
|
||||
result.sensorInsertionsInserted.forEach {
|
||||
uel.log(
|
||||
Action.CAREPORTAL,
|
||||
Sources.Xdrip,
|
||||
ValueWithUnit.Timestamp(it.timestamp),
|
||||
ValueWithUnit.TherapyEventType(it.type)
|
||||
)
|
||||
aapsLogger.debug(LTag.DATABASE, "Inserted sensor insertion $it")
|
||||
}
|
||||
}
|
||||
xdripSourcePlugin.sensorBatteryLevel = bundle.getInt(Intents.EXTRA_SENSOR_BATTERY, -1)
|
||||
return ret
|
||||
|
|
|
@ -1,25 +1,115 @@
|
|||
package app.aaps.plugins.source
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.work.WorkerFactory
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.testing.TestListenableWorkerBuilder
|
||||
import app.aaps.core.interfaces.receivers.Intents
|
||||
import app.aaps.core.interfaces.resources.ResourceHelper
|
||||
import app.aaps.core.interfaces.utils.DateUtil
|
||||
import app.aaps.core.interfaces.utils.T
|
||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
import app.aaps.core.utils.receivers.DataWorkerStorage
|
||||
import app.aaps.shared.impl.utils.DateUtilImpl
|
||||
import app.aaps.shared.tests.BundleMock
|
||||
import app.aaps.shared.tests.TestBase
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
|
||||
class XdripSourcePluginTest : TestBase() {
|
||||
|
||||
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 context: ContextWithInjector
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
xdripSourcePlugin = XdripSourcePlugin({ AndroidInjector { } }, rh, aapsLogger)
|
||||
`when`(context.applicationContext).thenReturn(context)
|
||||
`when`(context.androidInjector()).thenReturn(injector.androidInjector())
|
||||
xdripSourcePlugin = XdripSourcePlugin(injector, rh, aapsLogger)
|
||||
dateUtil = DateUtilImpl(context)
|
||||
dataWorkerStorage = DataWorkerStorage(context)
|
||||
}
|
||||
|
||||
private fun prepareWorker(
|
||||
sensorStartTime: Long? = dateUtil.now(),
|
||||
logNsSensorChange: Boolean = true,
|
||||
): Pair<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() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ dependencies {
|
|||
testImplementation(project(":shared:tests"))
|
||||
testImplementation(project(":implementation"))
|
||||
testImplementation(project(":plugins:aps"))
|
||||
androidTestImplementation(project(":shared:tests"))
|
||||
|
||||
// OpenHuman
|
||||
api(Libs.Squareup.Okhttp3.okhttp)
|
||||
|
@ -49,6 +50,10 @@ dependencies {
|
|||
// DataLayerListenerService
|
||||
api(Libs.Google.Android.PlayServices.wearable)
|
||||
|
||||
// Garmin
|
||||
api(Libs.connectiqSdk)
|
||||
androidTestImplementation(Libs.connectiqSdk)
|
||||
|
||||
kapt(Libs.Dagger.compiler)
|
||||
kapt(Libs.Dagger.androidProcessor)
|
||||
}
|
|
@ -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) })
|
||||
}
|
||||
}
|
|
@ -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]"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package app.aaps.plugins.sync.garmin
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.logging.AAPSLogger
|
||||
|
@ -14,10 +15,15 @@ import app.aaps.core.interfaces.rx.events.EventPreferenceChange
|
|||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
import app.aaps.database.entities.GlucoseValue
|
||||
import app.aaps.plugins.sync.R
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.math.BigDecimal
|
||||
import java.math.MathContext
|
||||
import java.math.RoundingMode
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.SocketAddress
|
||||
import java.net.URI
|
||||
import java.time.Clock
|
||||
|
@ -42,6 +48,7 @@ class GarminPlugin @Inject constructor(
|
|||
injector: HasAndroidInjector,
|
||||
aapsLogger: AAPSLogger,
|
||||
resourceHelper: ResourceHelper,
|
||||
private val context: Context,
|
||||
private val loopHub: LoopHub,
|
||||
private val rxBus: RxBus,
|
||||
private val sp: SP,
|
||||
|
@ -57,7 +64,19 @@ class GarminPlugin @Inject constructor(
|
|||
) {
|
||||
/** HTTP Server for local HTTP server communication (device app requests values) .*/
|
||||
private var server: HttpServer? = null
|
||||
var garminMessenger: GarminMessenger? = null
|
||||
|
||||
/** Garmin ConnectIQ application id for native communication. Phone pushes values. */
|
||||
private val glucoseAppIds = mapOf(
|
||||
"C9E90EE7E6924829A8B45E7DAFFF5CB4" to "GlucoseWatch_Dev",
|
||||
"1107CA6C2D5644B998D4BCB3793F2B7C" to "GlucoseDataField_Dev",
|
||||
"928FE19A4D3A4259B50CB6F9DDAF0F4A" to "GlucoseWidget_Dev",
|
||||
"662DFCF7F5A147DE8BD37F09574ADB11" to "GlucoseWatch",
|
||||
"815C7328C21248C493AD9AC4682FE6B3" to "GlucoseDataField",
|
||||
"4BDDCC1740084A1FAB83A3B2E2FCF55B" to "GlucoseWidget",
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
private val disposable = CompositeDisposable()
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -68,10 +87,24 @@ class GarminPlugin @Inject constructor(
|
|||
var newValue: Condition = valueLock.newCondition()
|
||||
private var lastGlucoseValueTimestamp: Long? = null
|
||||
private val glucoseUnitStr get() = if (loopHub.glucoseUnit == GlucoseUnit.MGDL) "mgdl" else "mmoll"
|
||||
private val garminAapsKey get() = sp.getString("garmin_aaps_key", "")
|
||||
|
||||
private fun onPreferenceChange(event: EventPreferenceChange) {
|
||||
aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}")
|
||||
setupHttpServer()
|
||||
when (event.changedKey) {
|
||||
"communication_debug_mode" -> setupGarminMessenger()
|
||||
"communication_http", "communication_http_port" -> setupHttpServer()
|
||||
"garmin_aaps_key" -> sendPhoneAppMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupGarminMessenger() {
|
||||
val enableDebug = sp.getBoolean("communication_ciq_debug_mode", false)
|
||||
garminMessenger?.dispose()
|
||||
garminMessenger = null
|
||||
aapsLogger.info(LTag.GARMIN, "initialize IQ messenger in debug=$enableDebug")
|
||||
garminMessenger = GarminMessenger(
|
||||
aapsLogger, context, glucoseAppIds, {_, _ -> },
|
||||
true, enableDebug).also { disposable.add(it) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -83,19 +116,34 @@ class GarminPlugin @Inject constructor(
|
|||
.observeOn(Schedulers.io())
|
||||
.subscribe(::onPreferenceChange)
|
||||
)
|
||||
disposable.add(
|
||||
rxBus
|
||||
.toObservable(EventNewBG::class.java)
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(::onNewBloodGlucose)
|
||||
)
|
||||
setupHttpServer()
|
||||
if (garminAapsKey.isNotEmpty())
|
||||
setupGarminMessenger()
|
||||
}
|
||||
|
||||
private fun setupHttpServer() {
|
||||
setupHttpServer(Duration.ZERO)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun setupHttpServer(wait: Duration) {
|
||||
if (sp.getBoolean("communication_http", false)) {
|
||||
val port = sp.getInt("communication_http_port", 28891)
|
||||
if (server != null && server?.port == port) return
|
||||
aapsLogger.info(LTag.GARMIN, "starting HTTP server on $port")
|
||||
server?.close()
|
||||
server = HttpServer(aapsLogger, port).apply {
|
||||
registerEndpoint("/get", ::onGetBloodGlucose)
|
||||
registerEndpoint("/carbs", ::onPostCarbs)
|
||||
registerEndpoint("/connect", ::onConnectPump)
|
||||
registerEndpoint("/get", requestHandler(::onGetBloodGlucose))
|
||||
registerEndpoint("/carbs", requestHandler(::onPostCarbs))
|
||||
registerEndpoint("/connect", requestHandler(::onConnectPump))
|
||||
registerEndpoint("/sgv.json", requestHandler(::onSgv))
|
||||
awaitReady(wait)
|
||||
}
|
||||
} else if (server != null) {
|
||||
aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
|
||||
|
@ -104,7 +152,7 @@ class GarminPlugin @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
public override fun onStop() {
|
||||
disposable.clear()
|
||||
aapsLogger.info(LTag.GARMIN, "Stop")
|
||||
server?.close()
|
||||
|
@ -128,6 +176,36 @@ class GarminPlugin @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun onConnectDevice(device: GarminDevice) {
|
||||
if (garminAapsKey.isNotEmpty()) {
|
||||
aapsLogger.info(LTag.GARMIN, "onConnectDevice $device sending glucose")
|
||||
sendPhoneAppMessage(device)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendPhoneAppMessage(device: GarminDevice) {
|
||||
garminMessenger?.sendMessage(device, getGlucoseMessage())
|
||||
}
|
||||
|
||||
private fun sendPhoneAppMessage() {
|
||||
garminMessenger?.sendMessage(getGlucoseMessage())
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun getGlucoseMessage() = mapOf<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. */
|
||||
@VisibleForTesting
|
||||
fun getGlucoseValues(): List<GlucoseValue> {
|
||||
|
@ -161,21 +239,33 @@ class GarminPlugin @Inject constructor(
|
|||
val glucoseMgDl: Int = glucose.value.roundToInt()
|
||||
encodedGlucose.add(timeSec, glucoseMgDl)
|
||||
}
|
||||
aapsLogger.info(
|
||||
LTag.GARMIN,
|
||||
"retrieved ${glucoseValues.size} last ${Date(glucoseValues.lastOrNull()?.timestamp ?: 0L)} ${encodedGlucose.size}"
|
||||
)
|
||||
return encodedGlucose.encodedBase64()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun requestHandler(action: (URI) -> CharSequence) = {
|
||||
caller: SocketAddress, uri: URI, _: String? ->
|
||||
val key = garminAapsKey
|
||||
val deviceKey = getQueryParameter(uri, "key")
|
||||
if (key.isNotEmpty() && key != deviceKey) {
|
||||
aapsLogger.warn(LTag.GARMIN, "Invalid AAPS Key from $caller, got '$deviceKey' want '$key' $uri")
|
||||
sendPhoneAppMessage()
|
||||
Thread.sleep(1000L)
|
||||
HttpURLConnection.HTTP_UNAUTHORIZED to "{}"
|
||||
} else {
|
||||
aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri")
|
||||
HttpURLConnection.HTTP_OK to action(uri).also {
|
||||
aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri, result: $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Responses to get glucose value request by the device.
|
||||
*
|
||||
* Also, gets the heart rate readings from the device.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onGetBloodGlucose(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
|
||||
aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri")
|
||||
fun onGetBloodGlucose(uri: URI): CharSequence {
|
||||
receiveHeartRate(uri)
|
||||
val profileName = loopHub.currentProfileName
|
||||
val waitSec = getQueryParameter(uri, "wait", 0L)
|
||||
|
@ -189,9 +279,7 @@ class GarminPlugin @Inject constructor(
|
|||
}
|
||||
jo.addProperty("profile", profileName.first().toString())
|
||||
jo.addProperty("connected", loopHub.isConnected)
|
||||
return jo.toString().also {
|
||||
aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri, result: $it")
|
||||
}
|
||||
return jo.toString()
|
||||
}
|
||||
|
||||
private fun getQueryParameter(uri: URI, name: String) = (uri.query ?: "")
|
||||
|
@ -223,6 +311,19 @@ class GarminPlugin @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun toLong(v: Any?) = (v as? Number?)?.toLong() ?: 0L
|
||||
|
||||
@VisibleForTesting
|
||||
fun receiveHeartRate(msg: Map<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
|
||||
fun receiveHeartRate(uri: URI) {
|
||||
val avg: Int = getQueryParameter(uri, "hr", 0L).toInt()
|
||||
|
@ -237,20 +338,18 @@ class GarminPlugin @Inject constructor(
|
|||
private fun receiveHeartRate(
|
||||
samplingStart: Instant, samplingEnd: Instant,
|
||||
avg: Int, device: String?, test: Boolean) {
|
||||
aapsLogger.info(LTag.GARMIN, "average heart rate $avg BPM test=$test")
|
||||
aapsLogger.info(LTag.GARMIN, "average heart rate $avg BPM $samplingStart to $samplingEnd")
|
||||
if (test) return
|
||||
if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) {
|
||||
loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device)
|
||||
} else {
|
||||
} else if (avg > 0) {
|
||||
aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd")
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles carb notification from the device. */
|
||||
@VisibleForTesting
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onPostCarbs(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
|
||||
aapsLogger.info(LTag.GARMIN, "carbs from $caller, req: $uri")
|
||||
fun onPostCarbs(uri: URI): CharSequence {
|
||||
postCarbs(getQueryParameter(uri, "carbs", 0L).toInt())
|
||||
return ""
|
||||
}
|
||||
|
@ -263,9 +362,7 @@ class GarminPlugin @Inject constructor(
|
|||
|
||||
/** Handles pump connected notification that the user entered on the Garmin device. */
|
||||
@VisibleForTesting
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onConnectPump(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence {
|
||||
aapsLogger.info(LTag.GARMIN, "connect from $caller, req: $uri")
|
||||
fun onConnectPump(uri: URI): CharSequence {
|
||||
val minutes = getQueryParameter(uri, "disconnectMinutes", 0L).toInt()
|
||||
if (minutes > 0) {
|
||||
loopHub.disconnectPump(minutes)
|
||||
|
@ -277,4 +374,61 @@ class GarminPlugin @Inject constructor(
|
|||
jo.addProperty("connected", loopHub.isConnected)
|
||||
return jo.toString()
|
||||
}
|
||||
|
||||
private fun glucoseSlopeMgDlPerMilli(glucose1: GlucoseValue, glucose2: GlucoseValue): Double {
|
||||
return (glucose2.value - glucose1.value) / (glucose2.timestamp - glucose1.timestamp)
|
||||
}
|
||||
|
||||
/** Returns glucose values in Nightscout/Xdrip format. */
|
||||
@VisibleForTesting
|
||||
fun onSgv(uri: URI): CharSequence {
|
||||
val count = getQueryParameter(uri,"count", 24L)
|
||||
.toInt().coerceAtMost(1000).coerceAtLeast(1)
|
||||
val briefMode = getQueryParameter(uri, "brief_mode", false)
|
||||
|
||||
// Guess a start time to get [count+1] readings. This is a heuristic that only works if we get readings
|
||||
// every 5 minutes and we're not missing readings. We truncate in case we get more readings but we'll
|
||||
// get less, e.g., in case we're missing readings for the last half hour. We get one extra reading,
|
||||
// to compute the glucose delta.
|
||||
val from = clock.instant().minus(Duration.ofMinutes(5L * (count + 1)))
|
||||
val glucoseValues = loopHub.getGlucoseValues(from, false)
|
||||
val joa = JsonArray()
|
||||
for (i in 0 until count.coerceAtMost(glucoseValues.size)) {
|
||||
val jo = JsonObject()
|
||||
val glucose = glucoseValues[i]
|
||||
if (!briefMode) {
|
||||
jo.addProperty("_id", glucose.id.toString())
|
||||
jo.addProperty("device", glucose.sourceSensor.toString())
|
||||
val timestamp = Instant.ofEpochMilli(glucose.timestamp)
|
||||
jo.addProperty("deviceString", timestamp.toString())
|
||||
jo.addProperty("sysTime", timestamp.toString())
|
||||
glucose.raw?.let { raw -> jo.addProperty("unfiltered", raw) }
|
||||
}
|
||||
jo.addProperty("date", glucose.timestamp)
|
||||
jo.addProperty("sgv", glucose.value.roundToInt())
|
||||
if (i + 1 < glucoseValues.size) {
|
||||
// Compute the 5 minute delta.
|
||||
val delta = 300_000.0 * glucoseSlopeMgDlPerMilli(glucoseValues[i + 1], glucose)
|
||||
jo.addProperty("delta", BigDecimal(delta, MathContext(3, RoundingMode.HALF_UP)))
|
||||
}
|
||||
jo.addProperty("direction", glucose.trendArrow.text)
|
||||
glucose.noise?.let { n -> jo.addProperty("noise", n) }
|
||||
if (i == 0) {
|
||||
when (loopHub.glucoseUnit) {
|
||||
GlucoseUnit.MGDL -> jo.addProperty("units_hint", "mgdl")
|
||||
GlucoseUnit.MMOL -> jo.addProperty("units_hint", "mmol")
|
||||
}
|
||||
jo.addProperty("iob", loopHub.insulinOnboard + loopHub.insulinBasalOnboard)
|
||||
loopHub.temporaryBasal.also {
|
||||
if (!it.isNaN()) {
|
||||
val temporaryBasalRateInPercent = (it * 100.0).toInt()
|
||||
jo.addProperty("tbr", temporaryBasalRateInPercent)
|
||||
}
|
||||
}
|
||||
jo.addProperty("cob", loopHub.carbsOnboard)
|
||||
}
|
||||
joa.add(jo)
|
||||
}
|
||||
return joa.toString()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -34,7 +34,7 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po
|
|||
|
||||
private val serverThread: Thread
|
||||
private val workerExecutor: Executor = Executors.newCachedThreadPool()
|
||||
private val endpoints: MutableMap<String, (SocketAddress, URI, String?) -> CharSequence> =
|
||||
private val endpoints: MutableMap<String, (SocketAddress, URI, String?) -> Pair<Int, CharSequence>> =
|
||||
ConcurrentHashMap()
|
||||
private var serverSocket: ServerSocket? = null
|
||||
private val readyLock = ReentrantLock()
|
||||
|
@ -76,7 +76,7 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po
|
|||
}
|
||||
|
||||
/** Register an endpoint (path) to handle requests. */
|
||||
fun registerEndpoint(path: String, endpoint: (SocketAddress, URI, String?) -> CharSequence) {
|
||||
fun registerEndpoint(path: String, endpoint: (SocketAddress, URI, String?) -> Pair<Int, CharSequence>) {
|
||||
aapsLogger.info(LTag.GARMIN, "Register: '$path'")
|
||||
endpoints[path] = endpoint
|
||||
}
|
||||
|
@ -127,8 +127,8 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po
|
|||
respond(HttpURLConnection.HTTP_NOT_FOUND, out)
|
||||
} else {
|
||||
try {
|
||||
val body = endpoint(s.remoteSocketAddress, uri, reqBody)
|
||||
respond(HttpURLConnection.HTTP_OK, body, "application/json", out)
|
||||
val (code, body) = endpoint(s.remoteSocketAddress, uri, reqBody)
|
||||
respond(code, body, "application/json", out)
|
||||
} catch (e: Exception) {
|
||||
aapsLogger.error(LTag.GARMIN, "endpoint " + uri.path + " failed", e)
|
||||
respond(HttpURLConnection.HTTP_INTERNAL_ERROR, out)
|
||||
|
|
|
@ -20,6 +20,12 @@ interface LoopHub {
|
|||
/** Returns the remaining bolus insulin on board. */
|
||||
val insulinOnboard: Double
|
||||
|
||||
/** Returns the basal insulin on board. */
|
||||
val insulinBasalOnboard: Double
|
||||
|
||||
/** Returns the remaining carbs on board. */
|
||||
val carbsOnboard: Double?
|
||||
|
||||
/** Returns true if the pump is connected. */
|
||||
val isConnected: Boolean
|
||||
|
||||
|
@ -48,4 +54,4 @@ interface LoopHub {
|
|||
avgHeartRate: Int,
|
||||
device: String?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction
|
|||
import app.aaps.core.interfaces.pump.DetailedBolusInfo
|
||||
import app.aaps.core.interfaces.queue.CommandQueue
|
||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
import app.aaps.core.main.graph.OverviewData
|
||||
import app.aaps.database.ValueWrapper
|
||||
import app.aaps.database.entities.EffectiveProfileSwitch
|
||||
import app.aaps.database.entities.GlucoseValue
|
||||
|
@ -42,6 +43,7 @@ class LoopHubImpl @Inject constructor(
|
|||
private val repo: AppRepository,
|
||||
private val userEntryLogger: UserEntryLogger,
|
||||
private val sp: SP,
|
||||
private val overviewData: OverviewData,
|
||||
) : LoopHub {
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -64,6 +66,14 @@ class LoopHubImpl @Inject constructor(
|
|||
override val insulinOnboard: Double
|
||||
get() = iobCobCalculator.calculateIobFromBolus().iob
|
||||
|
||||
/** Returns the remaining bolus and basal insulin on board. */
|
||||
override val insulinBasalOnboard :Double
|
||||
get() = iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().basaliob
|
||||
|
||||
/** Returns the remaining carbs on board. */
|
||||
override val carbsOnboard: Double?
|
||||
get() = overviewData.cobInfo(iobCobCalculator).displayCob
|
||||
|
||||
/** Returns true if the pump is connected. */
|
||||
override val isConnected: Boolean get() = !loop.isDisconnected
|
||||
|
||||
|
@ -142,4 +152,4 @@ class LoopHubImpl @Inject constructor(
|
|||
)
|
||||
repo.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,7 +167,8 @@ class NSClientV3Service : DaggerService() {
|
|||
rxBus.send(EventNSClientNewLog("◄ WS", "Subscribed for: ${response.optString("collections")}"))
|
||||
// during disconnection updated data is not received
|
||||
// thus run non WS load to get missing data
|
||||
nsClientV3Plugin.executeLoop("WS_CONNECT", forceNew = false)
|
||||
nsClientV3Plugin.initialLoadFinished = false
|
||||
nsClientV3Plugin.executeLoop("WS_CONNECT", forceNew = true)
|
||||
true
|
||||
} else {
|
||||
rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed"))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package app.aaps.plugins.sync.dataBroadcaster
|
||||
package app.aaps.plugins.sync.tizen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -41,7 +41,7 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DataBroadcastPlugin @Inject constructor(
|
||||
class TizenPlugin @Inject constructor(
|
||||
injector: HasAndroidInjector,
|
||||
aapsLogger: AAPSLogger,
|
||||
rh: ResourceHelper,
|
||||
|
@ -64,9 +64,9 @@ class DataBroadcastPlugin @Inject constructor(
|
|||
PluginDescription()
|
||||
.mainType(PluginType.SYNC)
|
||||
.pluginIcon(app.aaps.core.main.R.drawable.ic_watch)
|
||||
.pluginName(R.string.data_broadcaster)
|
||||
.shortName(R.string.data_broadcaster_short)
|
||||
.description(R.string.data_broadcaster_description),
|
||||
.pluginName(R.string.tizen)
|
||||
.shortName(R.string.tizen_short)
|
||||
.description(R.string.tizen_description),
|
||||
aapsLogger, rh, injector
|
||||
) {
|
||||
|
|
@ -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_title">Povolit odesílání do xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Odesílání dat do Garmin aplikace G-Watch Wear App</string>
|
||||
<string name="tizen">Samsung Tizen</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 -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Připojení k zařízení Garmin (Fenix, Edge, …)</string>
|
||||
|
|
|
@ -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_title">Activar las transmisiones a xDrip+</string>
|
||||
<!-- 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 -->
|
||||
<string name="garmin">Garmin</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="no_watch_connected">(Ningún reloj conectado)</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="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>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<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_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="openaps_short">OAPS</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_title">Activer les diffusions vers xDrip+.</string>
|
||||
<!-- 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 -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Connexion au périphérique Garmin (Fenix, Edge, …)</string>
|
||||
|
|
|
@ -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_title">Abilita trasmissioni a xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Trasmetti i dati all\'app G-Watch Wear di Garmin</string>
|
||||
<string name="tizen">Samsung Tizen</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 -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Connessione al dispositivo Garmin (Fenix, Edge, …)</string>
|
||||
|
|
|
@ -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_title">Įjungti duomenų perdavimą į xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster">Data Broadcaster</string>
|
||||
<!-- GarminPlugin -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Susiejimas su Garmin (Fenix, Edge, …)</string>
|
||||
|
|
|
@ -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_title">Aktiver sending til xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Kringkast data til Garmin\'s G-Watch Wear app</string>
|
||||
<string name="tizen">Samsung Tizen</string>
|
||||
<string name="tizen_short">TIZ</string>
|
||||
<string name="tizen_description">Kringkast data til Samsung\'s G-Watch Wear app (Tizen OS)</string>
|
||||
<!-- GarminPlugin -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Tilkobling til Garmin-enheter (Fenix, Edge,…)</string>
|
||||
|
|
|
@ -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_title">Activeer uitzendingen naar xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Verzend gegevens naar Garmin\'s G-Watch Wear App</string>
|
||||
<!-- GarminPlugin -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Verbinding met Garmin apparaat (Fenix, Edge, …)</string>
|
||||
|
|
|
@ -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_title">Włącz nadawanie do xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Transmisja danych do aplikacji G-Watch Garmin</string>
|
||||
<!-- GarminPlugin -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Połączenie z urządzeniem Garmin (Fenix, Edge, …)</string>
|
||||
|
|
|
@ -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_title">Activează transmisiuni spre xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Transmitere date către aplicaţia G-Watch Wear pe Garmin</string>
|
||||
<string name="tizen">Samsung Tizen</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 -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Conexiune la dispozitivul Garmin (Fenix, Edge, …)</string>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<string name="terms_of_use">Условия пользования</string>
|
||||
<string name="please_read__information">Внимательно ознакомьтесь со следующей информацией и примите условия использования.</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="boluses">Болюсы</string>
|
||||
<string name="extended_boluses">Пролонгированные болюсы</string>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<string name="device_model">Модель устройства</string>
|
||||
<string name="screen_dimensions">Размеры экрана</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="nightscout_url">URL-адрес Nightscout</string>
|
||||
<string name="nightscout_api_secret">Секретный ключ Nightscout API</string>
|
||||
|
|
|
@ -117,8 +117,9 @@
|
|||
<string name="xdrip_local_broadcasts_summary">Отправлять данные о глюкозе и терапии на xDrip+. Источником данных должен быть выбран \"xDrip+ Sync Follower\" а в настройках между приложениями надо включить - Принимать глюкозу/терапию</string>
|
||||
<string name="xdrip_local_broadcasts_title">Включить трансляции для xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Передавать данные приложению Garmin\'s G-Watch Wear</string>
|
||||
<string name="tizen">Samsung Tizen</string>
|
||||
<string name="tizen_short">TIZ</string>
|
||||
<string name="tizen_description">Передавать данные приложению Samsung G-Watch Wear (OS Tizen)</string>
|
||||
<!-- GarminPlugin -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Подключение к устройству Garmin (Fenix, Edge, …)</string>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<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_connectivity">Zablokované možnosti pripojenia</string>
|
||||
<string name="no_connectivity">Žiadne pripojenie</string>
|
||||
<string name="unsupported_ns_version">Nepodporovaná verzia Nighscoutu</string>
|
||||
<string name="openaps_short">OAPS</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_title">Povoliť odosielanie do xDrip+.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Odosielanie dát do Garmin aplikácie G-Watch Wear</string>
|
||||
<string name="tizen">Samsung Tizen</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 -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Pripájanie k zariadeniu Garmin (Fénix, Edge, ...)</string>
|
||||
|
|
|
@ -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_title">xDrip+ \'a yayınları etkinleştirin.</string>
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Verileri Garmin\'in G-Watch Wear Uygulamasına yayınlayın</string>
|
||||
<string name="tizen">Samsung Tizen</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 -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Garmin cihazına bağlantı (Fenix, Edge,…)</string>
|
||||
|
|
|
@ -180,9 +180,9 @@
|
|||
|
||||
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster">Data Broadcaster</string>
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Broadcast data to Garmin\'s G-Watch Wear App</string>
|
||||
<string name="tizen">Samsung Tizen</string>
|
||||
<string name="tizen_short">TIZ</string>
|
||||
<string name="tizen_description">Broadcast data to Samsung\'s G-Watch Wear App (Tizen OS)</string>
|
||||
|
||||
<!-- GarminPlugin -->
|
||||
<string name="garmin">Garmin</string>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package app.aaps.plugins.sync.garmin
|
||||
|
||||
import android.content.Context
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.resources.ResourceHelper
|
||||
import app.aaps.core.interfaces.rx.events.EventNewBG
|
||||
|
@ -11,9 +12,14 @@ import dagger.android.HasAndroidInjector
|
|||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.ArgumentMatchers.anyLong
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.atMost
|
||||
import org.mockito.Mockito.mock
|
||||
|
@ -21,19 +27,28 @@ import org.mockito.Mockito.times
|
|||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.atLeastOnce
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.whenever
|
||||
import java.net.ConnectException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.SocketAddress
|
||||
import java.net.URI
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.locks.Condition
|
||||
import kotlin.ranges.LongProgression.Companion.fromClosedRange
|
||||
|
||||
class GarminPluginTest: TestBase() {
|
||||
private lateinit var gp: GarminPlugin
|
||||
|
||||
@Mock private lateinit var rh: ResourceHelper
|
||||
@Mock private lateinit var sp: SP
|
||||
@Mock private lateinit var context: Context
|
||||
@Mock private lateinit var loopHub: LoopHub
|
||||
private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC"))
|
||||
|
||||
|
@ -44,14 +59,23 @@ class GarminPluginTest: TestBase() {
|
|||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
gp = GarminPlugin(injector, aapsLogger, rh, loopHub, rxBus, sp)
|
||||
gp = GarminPlugin(injector, aapsLogger, rh, context, loopHub, rxBus, sp)
|
||||
gp.clock = clock
|
||||
`when`(loopHub.currentProfileName).thenReturn("Default")
|
||||
`when`(sp.getBoolean(anyString(), anyBoolean())).thenAnswer { i -> i.arguments[1] }
|
||||
`when`(sp.getString(anyString(), anyString())).thenAnswer { i -> i.arguments[1] }
|
||||
`when`(sp.getInt(anyString(), anyInt())).thenAnswer { i -> i.arguments[1] }
|
||||
`when`(sp.getInt(eq("communication_http_port"), anyInt()))
|
||||
.thenReturn(28890)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun verifyNoFurtherInteractions() {
|
||||
verify(loopHub, atMost(2)).currentProfileName
|
||||
verify(loopHub, atMost(3)).insulinOnboard
|
||||
verify(loopHub, atMost(3)).insulinBasalOnboard
|
||||
verify(loopHub, atMost(3)).temporaryBasal
|
||||
verify(loopHub, atMost(3)).carbsOnboard
|
||||
verifyNoMoreInteractions(loopHub)
|
||||
}
|
||||
|
||||
|
@ -71,11 +95,23 @@ class GarminPluginTest: TestBase() {
|
|||
"device" to "Test_Device")
|
||||
|
||||
private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue(
|
||||
id = 10 * timestamp.toEpochMilli(),
|
||||
timestamp = timestamp.toEpochMilli(), raw = 90.0, value = value,
|
||||
trendArrow = GlucoseValue.TrendArrow.FLAT, noise = null,
|
||||
trendArrow = GlucoseValue.TrendArrow.FLAT, noise = 4.5,
|
||||
sourceSensor = GlucoseValue.SourceSensor.RANDOM
|
||||
)
|
||||
|
||||
@Test
|
||||
fun testReceiveHeartRateMap() {
|
||||
val hr = createHeartRate(80)
|
||||
gp.receiveHeartRate(hr, false)
|
||||
verify(loopHub).storeHeartRate(
|
||||
Instant.ofEpochSecond(hr["hrStart"] as Long),
|
||||
Instant.ofEpochSecond(hr["hrEnd"] as Long),
|
||||
80,
|
||||
hr["device"] as String)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReceiveHeartRateUri() {
|
||||
val hr = createHeartRate(99)
|
||||
|
@ -119,6 +155,132 @@ class GarminPluginTest: TestBase() {
|
|||
verify(loopHub).getGlucoseValues(from, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setupHttpServer_enabled() {
|
||||
`when`(sp.getBoolean("communication_http", false)).thenReturn(true)
|
||||
`when`(sp.getInt("communication_http_port", 28891)).thenReturn(28892)
|
||||
gp.setupHttpServer(Duration.ofSeconds(10))
|
||||
val reqUri = URI("http://127.0.0.1:28892/get")
|
||||
val resp = reqUri.toURL().openConnection() as HttpURLConnection
|
||||
assertEquals(200, resp.responseCode)
|
||||
|
||||
// Change port
|
||||
`when`(sp.getInt("communication_http_port", 28891)).thenReturn(28893)
|
||||
gp.setupHttpServer(Duration.ofSeconds(10))
|
||||
val reqUri2 = URI("http://127.0.0.1:28893/get")
|
||||
val resp2 = reqUri2.toURL().openConnection() as HttpURLConnection
|
||||
assertEquals(200, resp2.responseCode)
|
||||
|
||||
`when`(sp.getBoolean("communication_http", false)).thenReturn(false)
|
||||
gp.setupHttpServer(Duration.ofSeconds(10))
|
||||
assertThrows(ConnectException::class.java) {
|
||||
(reqUri2.toURL().openConnection() as HttpURLConnection).responseCode
|
||||
}
|
||||
gp.onStop()
|
||||
|
||||
verify(loopHub, times(2)).getGlucoseValues(anyObject(), eq(true))
|
||||
verify(loopHub, times(2)).insulinOnboard
|
||||
verify(loopHub, times(2)).temporaryBasal
|
||||
verify(loopHub, times(2)).isConnected
|
||||
verify(loopHub, times(2)).glucoseUnit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setupHttpServer_disabled() {
|
||||
gp.setupHttpServer(Duration.ofSeconds(10))
|
||||
val reqUri = URI("http://127.0.0.1:28890/get")
|
||||
assertThrows(ConnectException::class.java) {
|
||||
(reqUri.toURL().openConnection() as HttpURLConnection).responseCode
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestHandler_NoKey() {
|
||||
val uri = createUri(emptyMap())
|
||||
val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" }
|
||||
assertEquals(
|
||||
HttpURLConnection.HTTP_OK to "OK",
|
||||
handler(mock(SocketAddress::class.java), uri, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestHandler_KeyProvided() {
|
||||
val uri = createUri(mapOf("key" to "foo"))
|
||||
val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" }
|
||||
assertEquals(
|
||||
HttpURLConnection.HTTP_OK to "OK",
|
||||
handler(mock(SocketAddress::class.java), uri, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestHandler_KeyRequiredAndProvided() {
|
||||
`when`(sp.getString("garmin_aaps_key", "")).thenReturn("foo")
|
||||
val uri = createUri(mapOf("key" to "foo"))
|
||||
val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" }
|
||||
assertEquals(
|
||||
HttpURLConnection.HTTP_OK to "OK",
|
||||
handler(mock(SocketAddress::class.java), uri, null))
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestHandler_KeyRequired() {
|
||||
gp.garminMessenger = mock(GarminMessenger::class.java)
|
||||
|
||||
`when`(sp.getString("garmin_aaps_key", "")).thenReturn("foo")
|
||||
val uri = createUri(emptyMap())
|
||||
val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" }
|
||||
assertEquals(
|
||||
HttpURLConnection.HTTP_UNAUTHORIZED to "{}",
|
||||
handler(mock(SocketAddress::class.java), uri, null))
|
||||
|
||||
val captor = ArgumentCaptor.forClass(Any::class.java)
|
||||
verify(gp.garminMessenger)!!.sendMessage(captor.capture() ?: "")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val r = captor.value as Map<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
|
||||
fun testOnGetBloodGlucose() {
|
||||
`when`(loopHub.isConnected).thenReturn(true)
|
||||
|
@ -129,7 +291,7 @@ class GarminPluginTest: TestBase() {
|
|||
listOf(createGlucoseValue(Instant.ofEpochSecond(1_000))))
|
||||
val hr = createHeartRate(99)
|
||||
val uri = createUri(hr)
|
||||
val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null)
|
||||
val result = gp.onGetBloodGlucose(uri)
|
||||
assertEquals(
|
||||
"{\"encodedGlucose\":\"0A+6AQ==\"," +
|
||||
"\"remainingInsulin\":3.14," +
|
||||
|
@ -161,7 +323,7 @@ class GarminPluginTest: TestBase() {
|
|||
params["wait"] = 10
|
||||
val uri = createUri(params)
|
||||
gp.newValue = mock(Condition::class.java)
|
||||
val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null)
|
||||
val result = gp.onGetBloodGlucose(uri)
|
||||
assertEquals(
|
||||
"{\"encodedGlucose\":\"/wS6AQ==\"," +
|
||||
"\"remainingInsulin\":3.14," +
|
||||
|
@ -184,7 +346,7 @@ class GarminPluginTest: TestBase() {
|
|||
@Test
|
||||
fun testOnPostCarbs() {
|
||||
val uri = createUri(mapOf("carbs" to "12"))
|
||||
assertEquals("", gp.onPostCarbs(mock(SocketAddress::class.java), uri, null))
|
||||
assertEquals("", gp.onPostCarbs(uri))
|
||||
verify(loopHub).postCarbs(12)
|
||||
}
|
||||
|
||||
|
@ -192,7 +354,7 @@ class GarminPluginTest: TestBase() {
|
|||
fun testOnConnectPump_Disconnect() {
|
||||
val uri = createUri(mapOf("disconnectMinutes" to "20"))
|
||||
`when`(loopHub.isConnected).thenReturn(false)
|
||||
assertEquals("{\"connected\":false}", gp.onConnectPump(mock(SocketAddress::class.java), uri, null))
|
||||
assertEquals("{\"connected\":false}", gp.onConnectPump(uri))
|
||||
verify(loopHub).disconnectPump(20)
|
||||
verify(loopHub).isConnected
|
||||
}
|
||||
|
@ -201,8 +363,69 @@ class GarminPluginTest: TestBase() {
|
|||
fun testOnConnectPump_Connect() {
|
||||
val uri = createUri(mapOf("disconnectMinutes" to "0"))
|
||||
`when`(loopHub.isConnected).thenReturn(true)
|
||||
assertEquals("{\"connected\":true}", gp.onConnectPump(mock(SocketAddress::class.java), uri, null))
|
||||
assertEquals("{\"connected\":true}", gp.onConnectPump(uri))
|
||||
verify(loopHub).connectPump()
|
||||
verify(loopHub).isConnected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSgv_NoGlucose() {
|
||||
whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
|
||||
whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn(emptyList())
|
||||
assertEquals("[]", gp.onSgv(createUri(mapOf())))
|
||||
verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSgv_NoDelta() {
|
||||
whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
|
||||
whenever(loopHub.insulinOnboard).thenReturn(2.7)
|
||||
whenever(loopHub.insulinBasalOnboard).thenReturn(2.5)
|
||||
whenever(loopHub.temporaryBasal).thenReturn(0.8)
|
||||
whenever(loopHub.carbsOnboard).thenReturn(10.7)
|
||||
whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn(
|
||||
listOf(createGlucoseValue(
|
||||
clock.instant().minusSeconds(100L), 99.3)))
|
||||
assertEquals(
|
||||
"""[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7}]""",
|
||||
gp.onSgv(createUri(mapOf())))
|
||||
verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false)
|
||||
verify(loopHub).glucoseUnit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSgv() {
|
||||
whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL)
|
||||
whenever(loopHub.insulinOnboard).thenReturn(2.7)
|
||||
whenever(loopHub.insulinBasalOnboard).thenReturn(2.5)
|
||||
whenever(loopHub.temporaryBasal).thenReturn(0.8)
|
||||
whenever(loopHub.carbsOnboard).thenReturn(10.7)
|
||||
whenever(loopHub.getGlucoseValues(any(), eq(false))).thenAnswer { i ->
|
||||
val from = i.getArgument<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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -77,7 +77,7 @@ internal class HttpServerTest: TestBase() {
|
|||
HttpServer(aapsLogger, port).use { server ->
|
||||
server.registerEndpoint("/foo") { _: SocketAddress, uri: URI, _: String? ->
|
||||
assertEquals(URI("/foo"), uri)
|
||||
"test"
|
||||
HttpURLConnection.HTTP_OK to "test"
|
||||
}
|
||||
assertTrue(server.awaitReady(Duration.ofSeconds(10)))
|
||||
val resp = reqUri.toURL().openConnection() as HttpURLConnection
|
||||
|
|
|
@ -5,6 +5,7 @@ import app.aaps.core.interfaces.aps.Loop
|
|||
import app.aaps.core.interfaces.constraints.Constraint
|
||||
import app.aaps.core.interfaces.constraints.ConstraintsChecker
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.iob.CobInfo
|
||||
import app.aaps.core.interfaces.iob.IobCobCalculator
|
||||
import app.aaps.core.interfaces.iob.IobTotal
|
||||
import app.aaps.core.interfaces.logging.UserEntryLogger
|
||||
|
@ -13,6 +14,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction
|
|||
import app.aaps.core.interfaces.pump.DetailedBolusInfo
|
||||
import app.aaps.core.interfaces.queue.CommandQueue
|
||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
import app.aaps.core.main.graph.OverviewData
|
||||
import app.aaps.database.ValueWrapper
|
||||
import app.aaps.database.entities.EffectiveProfileSwitch
|
||||
import app.aaps.database.entities.GlucoseValue
|
||||
|
@ -54,6 +56,7 @@ class LoopHubTest: TestBase() {
|
|||
@Mock lateinit var repo: AppRepository
|
||||
@Mock lateinit var userEntryLogger: UserEntryLogger
|
||||
@Mock lateinit var sp: SP
|
||||
@Mock lateinit var overviewData: OverviewData
|
||||
|
||||
private lateinit var loopHub: LoopHubImpl
|
||||
private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC"))
|
||||
|
@ -62,7 +65,7 @@ class LoopHubTest: TestBase() {
|
|||
fun setup() {
|
||||
loopHub = LoopHubImpl(
|
||||
aapsLogger, commandQueue, constraints, iobCobCalculator, loop,
|
||||
profileFunction, repo, userEntryLogger, sp
|
||||
profileFunction, repo, userEntryLogger, sp, overviewData
|
||||
)
|
||||
loopHub.clock = clock
|
||||
}
|
||||
|
@ -76,9 +79,10 @@ class LoopHubTest: TestBase() {
|
|||
verifyNoMoreInteractions(profileFunction)
|
||||
verifyNoMoreInteractions(repo)
|
||||
verifyNoMoreInteractions(userEntryLogger)
|
||||
verifyNoMoreInteractions(overviewData)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test
|
||||
fun testCurrentProfile() {
|
||||
val profile = mock(Profile::class.java)
|
||||
`when`(profileFunction.getProfile()).thenReturn(profile)
|
||||
|
@ -109,6 +113,22 @@ class LoopHubTest: TestBase() {
|
|||
verify(iobCobCalculator, times(1)).calculateIobFromBolus()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBasalOnBoard() {
|
||||
val iobBasal = IobTotal(time = 0).apply { basaliob = 23.9 }
|
||||
`when`(iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended()).thenReturn(iobBasal)
|
||||
assertEquals(23.9, loopHub.insulinBasalOnboard, 1e-10)
|
||||
verify(iobCobCalculator, times(1)).calculateIobFromTempBasalsIncludingConvertedExtended()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCarbsOnBoard() {
|
||||
val cobInfo = CobInfo(0, 12.0, 0.0)
|
||||
`when`(overviewData.cobInfo(iobCobCalculator)).thenReturn(cobInfo)
|
||||
assertEquals(12.0, loopHub.carbsOnboard)
|
||||
verify(overviewData, times(1)).cobInfo(iobCobCalculator)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsConnected() {
|
||||
`when`(loop.isDisconnected).thenReturn(false)
|
||||
|
@ -247,4 +267,4 @@ class LoopHubTest: TestBase() {
|
|||
samplingStart, samplingEnd, 101, "Test Device")
|
||||
verify(repo).runTransaction(InsertOrUpdateHeartRateTransaction(hr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package app.aaps.plugins.sync.dataBroadcaster
|
||||
package app.aaps.plugins.sync.tizen
|
||||
|
||||
import app.aaps.core.interfaces.aps.AutosensDataStore
|
||||
import app.aaps.core.interfaces.aps.Loop
|
||||
|
@ -27,7 +27,7 @@ import org.mockito.ArgumentMatchers.anyLong
|
|||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
|
||||
internal class DataBroadcastPluginTest : TestBaseWithProfile() {
|
||||
internal class TizenPluginTest : TestBaseWithProfile() {
|
||||
|
||||
@Mock lateinit var defaultValueHelper: DefaultValueHelper
|
||||
@Mock lateinit var loop: Loop
|
||||
|
@ -36,13 +36,13 @@ internal class DataBroadcastPluginTest : TestBaseWithProfile() {
|
|||
@Mock lateinit var autosensDataStore: AutosensDataStore
|
||||
@Mock lateinit var processedDeviceStatusData: ProcessedDeviceStatusData
|
||||
|
||||
private lateinit var sut: DataBroadcastPlugin
|
||||
private lateinit var sut: TizenPlugin
|
||||
|
||||
private val injector = HasAndroidInjector { AndroidInjector { } }
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
sut = DataBroadcastPlugin(
|
||||
sut = TizenPlugin(
|
||||
injector, aapsLogger, rh, aapsSchedulers, context, dateUtil, fabricPrivacy, rxBus, iobCobCalculator, profileFunction, defaultValueHelper, processedDeviceStatusData,
|
||||
loop, activePlugin, receiverStatusStore, config, glucoseStatusProvider, decimalFormatter
|
||||
)
|
|
@ -28,23 +28,23 @@
|
|||
<string name="combo_reservoir_normal">Нормальный</string>
|
||||
<string name="combo_notification_check_time_date">Необходимо обновить часы помпы</string>
|
||||
<string name="combo_pump_tbr_cancelled_warrning">Предупреждение об отмене скорости временного базала подтверждено</string>
|
||||
<string name="combo_error_no_connection_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_bolus_verification_failed">Подача болюса и проверка истории помпы не состоялись, пожалуйста проверьте помпу. Если болюс был подан, он будет добавлен в назначения во время следующего соединения с помпой.</string>
|
||||
<string name="combo_error_no_connection_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_bolus_verification_failed">Ввод болюса и проверка истории помпы не состоялись, пожалуйста проверьте помпу. Если болюс был введен, он будет добавлен в назначения во время следующего соединения с помпой.</string>
|
||||
<string name="combo_reservoir_level_insufficient_for_bolus">В резервуаре недостаточно инсулина для болюса</string>
|
||||
<string name="combo_invalid_setup">Недопустимые установки помпы, проверьте документацию и убедитесь, что меню Quick Info называется QUICK INFO, используя приложение 360 для конфигурации помпы.</string>
|
||||
<string name="combo_actvity_reading_basal_profile">Чтение базального профиля</string>
|
||||
<string name="combo_bolus_rejected_due_to_pump_history_change">История событий помпы изменилась с момента вычисления болюса. Болюс не подан. Пожалуйста пересчитайте потребность в болюсе.</string>
|
||||
<string name="combo_error_updating_treatment_record">Болюс подан успешно, но запись о назначении не сделана. Это может быть вызвано тем, что за последние две минуты назначено два болюса одного объема. Пожалуйста проверьте историю событий помпы и записи о назначениях и добавьте пропущенные записи из Портала назначений. Не добавляйте записи с одним и тем же временем и одинаковым объемом.</string>
|
||||
<string name="combo_bolus_rejected_due_to_pump_history_change">История событий помпы изменилась с момента вычисления болюса. Болюс не введен. Пожалуйста пересчитайте потребность в болюсе.</string>
|
||||
<string name="combo_error_updating_treatment_record">Болюс введен успешно, но запись о назначении не сделана. Это может быть вызвано тем, что за последние две минуты назначено два болюса одного объема. Пожалуйста проверьте историю событий помпы и записи о назначениях и добавьте пропущенные записи из Портала назначений. Не добавляйте записи с одним и тем же временем и одинаковым объемом.</string>
|
||||
<string name="combo_high_temp_rejected_due_to_pump_history_changes">Временная верхняя цель отклонена т. к. калькуляция не учитывала недавние изменения в истории событий помпы</string>
|
||||
<string name="combo_activity_checking_pump_state">Обновление статуса помпы</string>
|
||||
<string name="combo_warning_pump_basal_rate_changed">Скорость базала на помпе изменилась и вскоре будет обновлена</string>
|
||||
<string name="combo_error_failure_reading_changed_basal_rate">Скорость базала на помпе изменилась, но учесть ее не удалось</string>
|
||||
<string name="combo_activity_checking_for_history_changes">Проверка изменений в истории событий помпы</string>
|
||||
<string name="combo_error_multiple_boluses_with_identical_timestamp">Только что импортировано несколько болюсов с одинаковым количеством инсулина в течение одной минуты. В лог лечения может быть добавлена только одна запись. Пожалуйста проверьте помпу и вручную введите запись о болюсе через вкладку портала назначений. Убедитесь, что данному времени соответствует только одна запись о болюсе.</string>
|
||||
<string name="combo_check_date">Новый болюс старше 24 часов или запись относится к будущему. Пожалуйста убедитесь что дата на помпе установлена правильно.</string>
|
||||
<string name="combo_suspious_bolus_time">Время/дата поданного болюса неверны. Вероятно, кол-во активного инсулина IOB также неверно. Проверьте время/дату помпы.</string>
|
||||
<string name="combo_check_date">Новый болюс старше 24 часов или запись относится к будущему. Убедитесь что дата на помпе установлена правильно.</string>
|
||||
<string name="combo_suspious_bolus_time">Время/дата болюса неверны. Вероятно, кол-во активного инсулина IOB также неверно. Проверьте время/дату помпы.</string>
|
||||
<string name="combo_bolus_count">Отсчет болюса</string>
|
||||
<string name="combo_tbr_count">Отсчет временного базала TBR</string>
|
||||
<string name="bolusstopped">Болюс остановлен</string>
|
||||
|
|
|
@ -153,7 +153,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
|
|||
PumpHistoryEntryType.ClearAlarm,
|
||||
PumpHistoryEntryType.ChangeAlarmNotifyMode,
|
||||
PumpHistoryEntryType.EnableDisableRemote,
|
||||
PumpHistoryEntryType.BGReceived,
|
||||
PumpHistoryEntryType.SensorAlert,
|
||||
PumpHistoryEntryType.ChangeTimeFormat,
|
||||
PumpHistoryEntryType.ChangeReservoirWarningTime,
|
||||
|
@ -188,7 +187,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
|
|||
PumpHistoryEntryType.ChangeWatchdogEnable,
|
||||
PumpHistoryEntryType.ChangeOtherDeviceID,
|
||||
PumpHistoryEntryType.ReadOtherDevicesIDs,
|
||||
PumpHistoryEntryType.BGReceived512,
|
||||
PumpHistoryEntryType.SensorStatus,
|
||||
PumpHistoryEntryType.ReadCaptureEventEnabled,
|
||||
PumpHistoryEntryType.ChangeCaptureEventEnable,
|
||||
|
@ -206,6 +204,12 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
|
|||
PumpHistoryEntryType.UnabsorbedInsulin,
|
||||
PumpHistoryEntryType.UnabsorbedInsulin512 -> RecordDecodeStatus.Ignored
|
||||
|
||||
PumpHistoryEntryType.BGReceived,
|
||||
PumpHistoryEntryType.BGReceived512 -> {
|
||||
decodeBgReceived(entry)
|
||||
RecordDecodeStatus.OK
|
||||
}
|
||||
|
||||
PumpHistoryEntryType.DailyTotals522,
|
||||
PumpHistoryEntryType.DailyTotals523,
|
||||
PumpHistoryEntryType.DailyTotals515,
|
||||
|
@ -297,7 +301,9 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
|
|||
}
|
||||
|
||||
private fun decodeBatteryActivity(entry: PumpHistoryEntry) {
|
||||
entry.displayableValue = if (entry.head[0] == 0.toByte()) "Battery Removed" else "Battery Replaced"
|
||||
val isRemoved = entry.head[0] == 0.toByte()
|
||||
entry.addDecodedData("isRemoved", isRemoved)
|
||||
entry.displayableValue = if (isRemoved) "Battery Removed" else "Battery Replaced"
|
||||
}
|
||||
|
||||
private fun decodeBasalProfileStart(entry: PumpHistoryEntry): RecordDecodeStatus {
|
||||
|
@ -407,8 +413,11 @@ class MedtronicPumpHistoryDecoder @Inject constructor(
|
|||
}
|
||||
|
||||
private fun decodeBgReceived(entry: PumpHistoryEntry) {
|
||||
entry.addDecodedData("amount", (ByteUtil.asUINT8(entry.getRawDataByIndex(0)) shl 3) + (ByteUtil.asUINT8(entry.getRawDataByIndex(3)) shr 5))
|
||||
entry.addDecodedData("meter", ByteUtil.substring(entry.rawData, 6, 3)) // index moved from 1 -> 0
|
||||
val glucoseMgdl = (ByteUtil.asUINT8(entry.head[0]) shl 3) + (ByteUtil.asUINT8(entry.datetime[2]) shr 5)
|
||||
val meterSerial = ByteUtil.shortHexStringWithoutSpaces(entry.body)
|
||||
entry.addDecodedData("GlucoseMgdl", glucoseMgdl)
|
||||
entry.addDecodedData("MeterSerial", meterSerial)
|
||||
entry.displayableValue = String.format("Glucose: %d mg/dl, Meter Serial: %s", glucoseMgdl, meterSerial)
|
||||
}
|
||||
|
||||
private fun decodeCalBGForPH(entry: PumpHistoryEntry) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger
|
|||
import app.aaps.core.interfaces.logging.LTag
|
||||
import app.aaps.core.interfaces.notifications.Notification
|
||||
import app.aaps.core.interfaces.plugin.ActivePlugin
|
||||
import app.aaps.core.interfaces.profile.ProfileUtil
|
||||
import app.aaps.core.interfaces.pump.DetailedBolusInfo
|
||||
import app.aaps.core.interfaces.pump.PumpSync
|
||||
import app.aaps.core.interfaces.pump.defs.PumpType
|
||||
|
@ -67,7 +68,8 @@ class MedtronicHistoryData @Inject constructor(
|
|||
val medtronicPumpStatus: MedtronicPumpStatus,
|
||||
private val pumpSync: PumpSync,
|
||||
private val pumpSyncStorage: PumpSyncStorage,
|
||||
private val uiInteraction: UiInteraction
|
||||
private val uiInteraction: UiInteraction,
|
||||
private val profileUtil: ProfileUtil
|
||||
) {
|
||||
|
||||
val allHistory: MutableList<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)
|
||||
*/
|
||||
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)
|
||||
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
|
||||
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)))
|
||||
|
@ -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>) {
|
||||
val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -30)
|
||||
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) {
|
||||
val lastPrimeFromAAPS = sp.getLong(eventSP, 0L)
|
||||
if (historyRecord.atechDateTime != lastPrimeFromAAPS) {
|
||||
|
|
|
@ -30,5 +30,6 @@ object MedtronicConst {
|
|||
const val LastPumpHistoryEntry = StatsPrefix + "pump_history_entry"
|
||||
const val LastPrime = StatsPrefix + "last_sent_prime"
|
||||
const val LastRewind = StatsPrefix + "last_sent_rewind"
|
||||
const val LastBatteryChange = StatsPrefix + "last_sent_battery_change"
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin
|
|||
import app.aaps.core.interfaces.pump.PumpSync
|
||||
import app.aaps.core.interfaces.resources.ResourceHelper
|
||||
import app.aaps.core.interfaces.sharedPreferences.SP
|
||||
import app.aaps.shared.tests.TestBase
|
||||
import app.aaps.shared.tests.TestBaseWithProfile
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil
|
||||
|
@ -16,15 +16,13 @@ import info.nightscout.pump.common.sync.PumpSyncStorage
|
|||
import org.mockito.Answers
|
||||
import org.mockito.Mock
|
||||
|
||||
open class MedtronicTestBase : TestBase() {
|
||||
open class MedtronicTestBase : TestBaseWithProfile() {
|
||||
|
||||
var rileyLinkUtil = RileyLinkUtil()
|
||||
|
||||
@Mock lateinit var pumpSync: PumpSync
|
||||
@Mock lateinit var pumpSyncStorage: PumpSyncStorage
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS) lateinit var activePlugin: ActivePlugin
|
||||
@Mock lateinit var sp: SP
|
||||
@Mock lateinit var rh: ResourceHelper
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS) override lateinit var activePlugin: ActivePlugin
|
||||
|
||||
lateinit var medtronicUtil: MedtronicUtil
|
||||
lateinit var decoder: MedtronicPumpHistoryDecoder
|
||||
|
@ -53,6 +51,24 @@ open class MedtronicTestBase : TestBase() {
|
|||
|
||||
}
|
||||
|
||||
fun getPumpHistoryEntryFromData(vararg elements: Int): PumpHistoryEntry {
|
||||
val data: MutableList<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> {
|
||||
val tbrs: MutableList<PumpHistoryEntry> = mutableListOf()
|
||||
val map: MutableMap<String?, PumpHistoryEntry?> = HashMap()
|
||||
|
|
|
@ -40,7 +40,7 @@ import org.mockito.Mock
|
|||
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
|
||||
medtronicHistoryData = MedtronicHistoryData(
|
||||
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
|
||||
medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction
|
||||
medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
package info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump
|
||||
|
||||
import app.aaps.core.interfaces.ui.UiInteraction
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.MedtronicPumpHistoryDecoder
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicDeviceType
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
|
||||
/**
|
||||
* Created by andy on 4/9/19.
|
||||
|
@ -10,6 +18,16 @@ import org.junit.jupiter.api.Test
|
|||
*/
|
||||
class PumpHistoryEntryUTest : MedtronicTestBase() {
|
||||
|
||||
@Mock lateinit var medtronicPumpStatus: MedtronicPumpStatus
|
||||
@Mock lateinit var uiInteraction: UiInteraction
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction)
|
||||
`when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel)
|
||||
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkIsAfter() {
|
||||
val dateObject = 20191010000000L
|
||||
|
@ -18,4 +36,21 @@ class PumpHistoryEntryUTest : MedtronicTestBase() {
|
|||
phe.atechDateTime = dateObject
|
||||
assertThat(phe.isAfter(queryObject)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodeBgReceived() {
|
||||
val bgRecord = getPumpHistoryEntryFromData(
|
||||
// head
|
||||
0x39, 0x15,
|
||||
// datetime (combined with glucose in mg/dl)
|
||||
0xC2, 0x25, 0xF3, 0x61, 0x17,
|
||||
// serial number
|
||||
0x12, 0x34, 0x56
|
||||
)
|
||||
val expectedGlucoseMgdl = 175
|
||||
val expectedMeterSerial = "123456"
|
||||
|
||||
assertThat(bgRecord.getDecodedDataEntry("GlucoseMgdl")).isEqualTo(expectedGlucoseMgdl)
|
||||
assertThat(bgRecord.getDecodedDataEntry("MeterSerial")).isEqualTo(expectedMeterSerial)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
package info.nightscout.androidaps.plugins.pump.medtronic.data
|
||||
|
||||
import app.aaps.core.interfaces.db.GlucoseUnit
|
||||
import app.aaps.core.interfaces.ui.UiInteraction
|
||||
import app.aaps.core.utils.DateTimeUtil
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.internal.LinkedTreeMap
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.MedtronicPumpHistoryDecoder
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntry
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntryType
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.data.dto.TempBasalPair
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicDeviceType
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import java.lang.reflect.Type
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -24,6 +30,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
|
|||
@BeforeEach
|
||||
fun setUp() {
|
||||
medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction)
|
||||
`when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel)
|
||||
decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil)
|
||||
}
|
||||
|
||||
|
@ -32,7 +39,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
|
|||
|
||||
val unitToTest = MedtronicHistoryData(
|
||||
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
|
||||
medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction
|
||||
medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil
|
||||
)
|
||||
|
||||
val gson = Gson()
|
||||
|
@ -75,7 +82,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
|
|||
medtronicUtil, decoder,
|
||||
medtronicPumpStatus,
|
||||
pumpSync,
|
||||
pumpSyncStorage, uiInteraction
|
||||
pumpSyncStorage, uiInteraction, profileUtil
|
||||
)
|
||||
|
||||
val gson = Gson()
|
||||
|
@ -110,4 +117,70 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processBgReceived_WithMgdl() {
|
||||
|
||||
val unitToTest = MedtronicHistoryData(
|
||||
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
|
||||
medtronicUtil, decoder,
|
||||
medtronicPumpStatus,
|
||||
pumpSync,
|
||||
pumpSyncStorage, uiInteraction, profileUtil
|
||||
)
|
||||
|
||||
val glucoseMgdl = 175
|
||||
|
||||
`when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn(GlucoseUnit.MGDL.asText)
|
||||
|
||||
val bgRecord = PumpHistoryEntry()
|
||||
bgRecord.setEntryType(medtronicUtil.medtronicPumpModel, PumpHistoryEntryType.BGReceived)
|
||||
bgRecord.addDecodedData("GlucoseMgdl", glucoseMgdl)
|
||||
bgRecord.addDecodedData("MeterSerial", "123456")
|
||||
|
||||
unitToTest.processBgReceived(listOf(bgRecord))
|
||||
|
||||
verify(pumpSync).insertFingerBgIfNewWithTimestamp(
|
||||
DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime),
|
||||
glucoseMgdl.toDouble(),
|
||||
GlucoseUnit.MGDL, null,
|
||||
bgRecord.pumpId,
|
||||
medtronicPumpStatus.pumpType,
|
||||
medtronicPumpStatus.serialNumber
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processBgReceived_WithMmol() {
|
||||
|
||||
val unitToTest = MedtronicHistoryData(
|
||||
packetInjector, aapsLogger, sp, rh, rxBus, activePlugin,
|
||||
medtronicUtil, decoder,
|
||||
medtronicPumpStatus,
|
||||
pumpSync,
|
||||
pumpSyncStorage, uiInteraction, profileUtil
|
||||
)
|
||||
val glucoseMgdl = 180
|
||||
val glucoseMmol = 10.0
|
||||
|
||||
`when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn(GlucoseUnit.MMOL.asText)
|
||||
|
||||
val bgRecord = PumpHistoryEntry()
|
||||
bgRecord.setEntryType(medtronicUtil.medtronicPumpModel, PumpHistoryEntryType.BGReceived)
|
||||
bgRecord.addDecodedData("GlucoseMgdl", glucoseMgdl)
|
||||
bgRecord.addDecodedData("MeterSerial", "123456")
|
||||
|
||||
unitToTest.processBgReceived(listOf(bgRecord))
|
||||
|
||||
verify(pumpSync).insertFingerBgIfNewWithTimestamp(
|
||||
DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime),
|
||||
glucoseMmol,
|
||||
GlucoseUnit.MMOL, null,
|
||||
bgRecord.pumpId,
|
||||
medtronicPumpStatus.pumpType,
|
||||
medtronicPumpStatus.serialNumber
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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_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_normal">Normální</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_activation_time_exceeded">Byl překročen čas aktivace</string>
|
||||
|
|
|
@ -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_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_normal">Normal</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_activation_time_exceeded">Tiempo de activación excedido</string>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<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_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_bolus">Dernier bolus</string>
|
||||
<string name="omnipod_common_overview_temp_basal_rate">Débit de Basal Temp.</string>
|
||||
|
|
|
@ -124,6 +124,7 @@
|
|||
<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_running">פועל</string>
|
||||
<string name="omnipod_common_pod_status_normal">נורמלי</string>
|
||||
<string name="omnipod_common_pod_status_suspended">מושהה</string>
|
||||
<string name="omnipod_common_pod_status_pod_fault">תקלה בפוד</string>
|
||||
<string name="omnipod_common_pod_status_activation_time_exceeded">חריגת זמן הפעלה</string>
|
||||
|
|
|
@ -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_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_normal">Normal</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_activation_time_exceeded">Aktiveringstiden er overskredet</string>
|
||||
|
|
|
@ -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_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_normal">Normaal</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_activation_time_exceeded">Activatie tijd verlopen</string>
|
||||
|
|
|
@ -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_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_normal">Normalny</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_activation_time_exceeded">Przekroczono czas aktywacji</string>
|
||||
|
|
|
@ -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_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_normal">Normal</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_activation_time_exceeded">Timp de activare depăşit</string>
|
||||
|
|
|
@ -124,6 +124,7 @@
|
|||
<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_running">Выполняется</string>
|
||||
<string name="omnipod_common_pod_status_normal">Норма</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_activation_time_exceeded">Превышено время активации</string>
|
||||
|
|
|
@ -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_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_normal">Normálny</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_activation_time_exceeded">Bol prekročený čas aktivácie</string>
|
||||
|
|
|
@ -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_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_normal">Normal</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_activation_time_exceeded">Aktivasyon süresi aşıldı</string>
|
||||
|
|
|
@ -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_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_normal">Normal</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_activation_time_exceeded">Activation time exceeded</string>
|
||||
|
|
|
@ -183,7 +183,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
|
|||
if (!podStateManager.isPodRunning) {
|
||||
uiInteraction.addNotification(
|
||||
Notification.OMNIPOD_POD_NOT_ATTACHED,
|
||||
"Pod not activated",
|
||||
rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod),
|
||||
Notification.NORMAL
|
||||
)
|
||||
} else {
|
||||
|
@ -991,9 +991,9 @@ class OmnipodDashPumpPlugin @Inject constructor(
|
|||
val extended = JSONObject()
|
||||
try {
|
||||
val podStatus = when {
|
||||
podStateManager.isPodRunning && podStateManager.isSuspended -> "suspended"
|
||||
podStateManager.isPodRunning -> "normal"
|
||||
else -> "no active Pod"
|
||||
podStateManager.isPodRunning && podStateManager.isSuspended -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_suspended).lowercase()
|
||||
podStateManager.isPodRunning -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_normal).lowercase()
|
||||
else -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod).lowercase()
|
||||
}
|
||||
status.put("status", podStatus)
|
||||
status.put("timestamp", dateUtil.toISOString(podStateManager.lastUpdatedSystem))
|
||||
|
|
|
@ -36,7 +36,7 @@ import org.mockito.invocation.InvocationOnMock
|
|||
@Suppress("SpellCheckingInspection")
|
||||
open class TestBaseWithProfile : TestBase() {
|
||||
|
||||
@Mock lateinit var activePlugin: ActivePlugin
|
||||
@Mock open lateinit var activePlugin: ActivePlugin
|
||||
@Mock lateinit var rh: ResourceHelper
|
||||
@Mock lateinit var iobCobCalculator: IobCobCalculator
|
||||
@Mock lateinit var fabricPrivacy: FabricPrivacy
|
||||
|
|
|
@ -284,31 +284,6 @@
|
|||
|
||||
</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
|
||||
android:id="@+id/delimiter"
|
||||
android:layout_width="fill_parent"
|
||||
|
@ -683,6 +658,31 @@
|
|||
|
||||
</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>
|
||||
|
||||
</ScrollView>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<string name="record_pump_site_change">Внести запись о смене места катетера помпы</string>
|
||||
<string name="record_insulin_cartridge_change">Внести запись о замене картриджа инсулина</string>
|
||||
<!-- InsulinDialog -->
|
||||
<string name="do_not_bolus_record_only">Не подавать болюс, только внести запись</string>
|
||||
<string name="do_not_bolus_record_only">Не вводить болюс, только внести запись</string>
|
||||
<!-- ProfileSwitchDialog -->
|
||||
<string name="reuse_profile_pct_hours">Повторно использовать %1$d%%%2$dч</string>
|
||||
<string name="timeshift_label">Сдвиг по времени</string>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue